From 71e720e94513b7d36f80223387719338fa5406f2 Mon Sep 17 00:00:00 2001 From: Alec Rippberger <127791530+alec-livefront@users.noreply.github.com> Date: Tue, 22 Apr 2025 23:09:07 -0500 Subject: [PATCH 1/8] fix(auth): clarify 2FA security key verification text Updates user interface text to improve clarity when prompting for security key verification during two-factor authentication. Ref: PM-20055 --- apps/browser/src/_locales/en/messages.json | 3 +++ apps/desktop/src/locales/en/messages.json | 3 +++ apps/web/src/connectors/webauthn-fallback.ts | 2 +- apps/web/src/locales/en/messages.json | 3 +++ .../src/angular/two-factor-auth/two-factor-auth.component.ts | 2 +- 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 73c64ba7fd4..462af12a352 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -886,6 +886,9 @@ "followTheStepsBelowToFinishLoggingIn": { "message": "Follow the steps below to finish logging in." }, + "followTheStepsBelowToFinishLoggingInWithSecurityKey": { + "message": "Follow the steps below to finish logging in with your security key." + }, "restartRegistration": { "message": "Restart registration" }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 8850cbe5a3f..6097543e50a 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3212,6 +3212,9 @@ "followTheStepsBelowToFinishLoggingIn": { "message": "Follow the steps below to finish logging in." }, + "followTheStepsBelowToFinishLoggingInWithSecurityKey": { + "message": "Follow the steps below to finish logging in with your security key." + }, "launchDuo": { "message": "Launch Duo in Browser" }, diff --git a/apps/web/src/connectors/webauthn-fallback.ts b/apps/web/src/connectors/webauthn-fallback.ts index 3561f922e03..43be5733973 100644 --- a/apps/web/src/connectors/webauthn-fallback.ts +++ b/apps/web/src/connectors/webauthn-fallback.ts @@ -86,7 +86,7 @@ document.addEventListener("DOMContentLoaded", async () => { titleForLargerScreens.innerText = localeService.t("verifyYourIdentity"); const subtitle = document.getElementById("subtitle"); - subtitle.innerText = localeService.t("followTheStepsBelowToFinishLoggingIn"); + subtitle.innerText = localeService.t("followTheStepsBelowToFinishLoggingInWithSecurityKey"); }); function start() { diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 56a98a661ef..05d29071731 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7280,6 +7280,9 @@ "followTheStepsBelowToFinishLoggingIn": { "message": "Follow the steps below to finish logging in." }, + "followTheStepsBelowToFinishLoggingInWithSecurityKey": { + "message": "Follow the steps below to finish logging in with your security key." + }, "launchDuo": { "message": "Launch Duo" }, diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts index 6cdf42b76da..aed9f9f07a5 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts @@ -362,7 +362,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { break; case TwoFactorProviderType.WebAuthn: this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ - pageSubtitle: this.i18nService.t("followTheStepsBelowToFinishLoggingIn"), + pageSubtitle: this.i18nService.t("followTheStepsBelowToFinishLoggingInWithSecurityKey"), pageIcon: TwoFactorAuthWebAuthnIcon, }); break; From 60fe8fa7b00dbb27a419e926e7d3dc4e376f92a4 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 23 Apr 2025 14:21:45 +0200 Subject: [PATCH 2/8] Add comments to send service to make it easier to follow (#14389) --- libs/common/src/tools/send/services/send.service.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 8d6e62e3b8c..cefd9942d29 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -50,7 +50,7 @@ export class SendService implements InternalSendServiceAbstraction { model: SendView, file: File | ArrayBuffer, password: string, - key?: SymmetricCryptoKey, + userKey?: SymmetricCryptoKey, ): Promise<[Send, EncArrayBuffer]> { let fileData: EncArrayBuffer = null; const send = new Send(); @@ -62,15 +62,19 @@ export class SendService implements InternalSendServiceAbstraction { send.deletionDate = model.deletionDate; send.expirationDate = model.expirationDate; if (model.key == null) { + // Sends use a seed, stored in the URL fragment. This seed is used to derive the key that is used for encryption. const key = await this.keyGenerationService.createKeyWithPurpose( 128, this.sendKeyPurpose, this.sendKeySalt, ); + // key.material is the seed that can be used to re-derive the key model.key = key.material; model.cryptoKey = key.derivedKey; } if (password != null) { + // Note: Despite being called key, the passwordKey is not used for encryption. + // It is used as a static proof that the client knows the password, and has the encryption key. const passwordKey = await this.keyGenerationService.deriveKeyFromPassword( password, model.key, @@ -78,11 +82,11 @@ export class SendService implements InternalSendServiceAbstraction { ); send.password = passwordKey.keyB64; } - if (key == null) { - key = await this.keyService.getUserKey(); + if (userKey == null) { + userKey = await this.keyService.getUserKey(); } // Key is not a SymmetricCryptoKey, but key material used to derive the cryptoKey - send.key = await this.encryptService.encrypt(model.key, key); + send.key = await this.encryptService.encrypt(model.key, userKey); send.name = await this.encryptService.encrypt(model.name, model.cryptoKey); send.notes = await this.encryptService.encrypt(model.notes, model.cryptoKey); if (send.type === SendType.Text) { From 8e1dfb7d214f3f457f688b5f693830d9355853d2 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Wed, 23 Apr 2025 09:59:05 -0400 Subject: [PATCH 3/8] PM-19281 localize relevant notification bar text (#14327) * PM-19281 localize todo text * update storybook --- .../content/components/cipher/cipher-action.ts | 8 ++++---- .../content/components/cipher/cipher-item.ts | 4 +++- .../ciphers/cipher-action.lit-stories.ts | 1 + .../lit-stories/notification/body.lit-stories.ts | 1 + .../content/components/notification/body.ts | 3 +++ .../content/components/notification/button-row.ts | 14 ++++++++------ .../content/components/notification/container.ts | 1 + .../content/components/notification/footer.ts | 1 + apps/browser/src/autofill/notification/bar.ts | 7 +++++-- 9 files changed, 27 insertions(+), 13 deletions(-) diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-action.ts b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts index aaa4b11d8a2..85698c87c67 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-action.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts @@ -8,24 +8,24 @@ export function CipherAction({ handleAction = () => { /* no-op */ }, + i18n, notificationType, theme, }: { handleAction?: (e: Event) => void; + i18n: { [key: string]: string }; notificationType: typeof NotificationTypes.Change | typeof NotificationTypes.Add; theme: Theme; }) { return notificationType === NotificationTypes.Change ? BadgeButton({ buttonAction: handleAction, - // @TODO localize - buttonText: "Update", + buttonText: i18n.notificationUpdate, theme, }) : EditButton({ buttonAction: handleAction, - // @TODO localize - buttonText: "Edit", + buttonText: i18n.notificationEdit, theme, }); } diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-item.ts b/apps/browser/src/autofill/content/components/cipher/cipher-item.ts index 96b44d2c0cc..8ab29860f3b 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-item.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-item.ts @@ -19,11 +19,13 @@ const cipherIconWidth = "24px"; export function CipherItem({ cipher, handleAction, + i18n, notificationType, theme = ThemeTypes.Light, }: { cipher: NotificationCipherData; handleAction?: (e: Event) => void; + i18n: { [key: string]: string }; notificationType?: NotificationType; theme: Theme; }) { @@ -34,7 +36,7 @@ export function CipherItem({ if (notificationType === NotificationTypes.Change || notificationType === NotificationTypes.Add) { cipherActionButton = html`
- ${CipherAction({ handleAction, notificationType, theme })} + ${CipherAction({ handleAction, i18n, notificationType, theme })}
`; } diff --git a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts index e597cddabe6..dd1ff816f06 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts @@ -7,6 +7,7 @@ import { CipherAction } from "../../cipher/cipher-action"; type Args = { handleAction?: (e: Event) => void; + i18n: { [key: string]: string }; notificationType: typeof NotificationTypes.Change | typeof NotificationTypes.Add; theme: Theme; }; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts index 32b4170d1da..13e2322a9f2 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts @@ -10,6 +10,7 @@ import { NotificationBody } from "../../notification/body"; type Args = { ciphers: NotificationCipherData[]; + i18n: { [key: string]: string }; notificationType: NotificationType; theme: Theme; handleEditOrUpdateAction: (e: Event) => void; diff --git a/apps/browser/src/autofill/content/components/notification/body.ts b/apps/browser/src/autofill/content/components/notification/body.ts index 66b580bde43..cc0fa359303 100644 --- a/apps/browser/src/autofill/content/components/notification/body.ts +++ b/apps/browser/src/autofill/content/components/notification/body.ts @@ -17,12 +17,14 @@ const { css } = createEmotion({ export function NotificationBody({ ciphers = [], + i18n, notificationType, theme = ThemeTypes.Light, handleEditOrUpdateAction, }: { ciphers?: NotificationCipherData[]; customClasses?: string[]; + i18n: { [key: string]: string }; notificationType?: NotificationType; theme: Theme; handleEditOrUpdateAction: (e: Event) => void; @@ -37,6 +39,7 @@ export function NotificationBody({ theme, children: CipherItem({ cipher, + i18n, notificationType, theme, handleAction: handleEditOrUpdateAction, diff --git a/apps/browser/src/autofill/content/components/notification/button-row.ts b/apps/browser/src/autofill/content/components/notification/button-row.ts index 8661f5957e1..3834da4269d 100644 --- a/apps/browser/src/autofill/content/components/notification/button-row.ts +++ b/apps/browser/src/autofill/content/components/notification/button-row.ts @@ -22,17 +22,19 @@ function getVaultIconByProductTier(productTierType?: ProductTierType): Option["i } export type NotificationButtonRowProps = { - theme: Theme; + folders?: FolderView[]; + i18n: { [key: string]: string }; + organizations?: OrgView[]; primaryButton: { text: string; handlePrimaryButtonClick: (args: any) => void; }; - folders?: FolderView[]; - organizations?: OrgView[]; + theme: Theme; }; export function NotificationButtonRow({ folders, + i18n, organizations, primaryButton, theme, @@ -40,7 +42,7 @@ export function NotificationButtonRow({ const currentUserVaultOption: Option = { icon: User, default: true, - text: "My vault", // @TODO localize + text: i18n.myVault, value: "0", }; const organizationOptions: Option[] = organizations?.length @@ -84,7 +86,7 @@ export function NotificationButtonRow({ ? [ { id: "organization", - label: "Vault", // @TODO localize + label: i18n.vault, options: organizationOptions, }, ] @@ -93,7 +95,7 @@ export function NotificationButtonRow({ ? [ { id: "folder", - label: "Folder", // @TODO localize + label: i18n.folder, options: folderOptions, }, ] diff --git a/apps/browser/src/autofill/content/components/notification/container.ts b/apps/browser/src/autofill/content/components/notification/container.ts index c29f58e116b..e1d098e3b09 100644 --- a/apps/browser/src/autofill/content/components/notification/container.ts +++ b/apps/browser/src/autofill/content/components/notification/container.ts @@ -59,6 +59,7 @@ export function NotificationContainer({ ciphers, notificationType: type, theme, + i18n, }) : null} ${NotificationFooter({ diff --git a/apps/browser/src/autofill/content/components/notification/footer.ts b/apps/browser/src/autofill/content/components/notification/footer.ts index 8ed69a96ad9..58a87ebc678 100644 --- a/apps/browser/src/autofill/content/components/notification/footer.ts +++ b/apps/browser/src/autofill/content/components/notification/footer.ts @@ -38,6 +38,7 @@ export function NotificationFooter({ ? NotificationButtonRow({ folders, organizations, + i18n, primaryButton: { handlePrimaryButtonClick: handleSaveAction, text: primaryButtonText, diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index d660790ee63..4e85d893178 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -53,6 +53,7 @@ function getI18n() { return { appName: chrome.i18n.getMessage("appName"), close: chrome.i18n.getMessage("close"), + collection: chrome.i18n.getMessage("collection"), folder: chrome.i18n.getMessage("folder"), loginSaveSuccess: chrome.i18n.getMessage("loginSaveSuccess"), loginSaveSuccessDetails: chrome.i18n.getMessage("loginSaveSuccessDetails"), @@ -63,10 +64,11 @@ function getI18n() { nextSecurityTaskAction: chrome.i18n.getMessage("nextSecurityTaskAction"), newItem: chrome.i18n.getMessage("newItem"), never: chrome.i18n.getMessage("never"), + myVault: chrome.i18n.getMessage("myVault"), notificationAddDesc: chrome.i18n.getMessage("notificationAddDesc"), notificationAddSave: chrome.i18n.getMessage("notificationAddSave"), notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"), - notificationChangeSave: chrome.i18n.getMessage("notificationChangeSave"), + notificationUpdate: chrome.i18n.getMessage("notificationChangeSave"), notificationEdit: chrome.i18n.getMessage("edit"), notificationUnlock: chrome.i18n.getMessage("notificationUnlock"), notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"), @@ -78,6 +80,7 @@ function getI18n() { typeLogin: chrome.i18n.getMessage("typeLogin"), updateLoginAction: chrome.i18n.getMessage("updateLoginAction"), updateLoginPrompt: chrome.i18n.getMessage("updateLoginPrompt"), + vault: chrome.i18n.getMessage("vault"), view: chrome.i18n.getMessage("view"), }; } @@ -200,7 +203,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) { const changeTemplate = document.getElementById("template-change") as HTMLTemplateElement; const changeButton = findElementById(changeTemplate, "change-save"); - changeButton.textContent = i18n.notificationChangeSave; + changeButton.textContent = i18n.notificationUpdate; const changeEditButton = findElementById(changeTemplate, "change-edit"); changeEditButton.textContent = i18n.notificationEdit; From 3aa1378c992df4da6e035ca4a89931c1cf39cb87 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Wed, 23 Apr 2025 16:59:14 +0200 Subject: [PATCH 4/8] Eliminate dead code from trial-initiation (#14378) --- .../abm-enterprise-content.component.html | 17 --- .../abm-enterprise-content.component.ts | 7 - .../content/abm-teams-content.component.html | 17 --- .../content/abm-teams-content.component.ts | 7 - .../cnet-enterprise-content.component.html | 17 --- .../cnet-enterprise-content.component.ts | 7 - .../cnet-individual-content.component.html | 17 --- .../cnet-individual-content.component.ts | 7 - .../content/cnet-teams-content.component.html | 17 --- .../content/cnet-teams-content.component.ts | 7 - .../content/default-content.component.html | 16 -- .../content/default-content.component.ts | 7 - .../content/enterprise-content.component.html | 44 ------ .../content/enterprise-content.component.ts | 7 - .../enterprise1-content.component.html | 44 ------ .../content/enterprise1-content.component.ts | 7 - .../enterprise2-content.component.html | 44 ------ .../content/enterprise2-content.component.ts | 7 - .../content/logo-badges.component.html | 11 -- .../content/logo-badges.component.ts | 7 - .../content/logo-cnet-5-stars.component.html | 23 --- .../content/logo-cnet-5-stars.component.ts | 7 - .../content/logo-cnet.component.html | 15 -- .../content/logo-cnet.component.ts | 7 - .../logo-company-testimonial.component.html | 28 ---- .../logo-company-testimonial.component.ts | 7 - .../content/logo-forbes.component.html | 15 -- .../content/logo-forbes.component.ts | 7 - .../content/logo-us-news.component.html | 5 - .../content/logo-us-news.component.ts | 7 - .../content/review-blurb.component.html | 13 -- .../content/review-blurb.component.ts | 13 -- .../content/review-logo.component.html | 18 --- .../content/review-logo.component.ts | 13 -- .../secrets-manager-content.component.html | 30 ---- .../secrets-manager-content.component.ts | 80 ---------- .../content/teams-content.component.html | 17 --- .../content/teams-content.component.ts | 7 - .../content/teams1-content.component.html | 35 ----- .../content/teams1-content.component.ts | 7 - .../content/teams2-content.component.html | 35 ----- .../content/teams2-content.component.ts | 7 - .../content/teams3-content.component.html | 26 ---- .../content/teams3-content.component.ts | 7 - ...-manager-trial-free-stepper.component.html | 45 ------ ...ts-manager-trial-free-stepper.component.ts | 90 ----------- ...-manager-trial-paid-stepper.component.html | 67 -------- ...ts-manager-trial-paid-stepper.component.ts | 144 ------------------ .../secrets-manager-trial.component.html | 44 ------ .../secrets-manager-trial.component.ts | 32 ---- .../trial-initiation.module.ts | 59 +------ 51 files changed, 1 insertion(+), 1223 deletions(-) delete mode 100644 apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/default-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/default-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-badges.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-badges.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/review-blurb.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/review-blurb.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/review-logo.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/review-logo.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/teams-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/teams-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/teams1-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/teams1-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/teams2-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/teams2-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/content/teams3-content.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/content/teams3-content.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.ts delete mode 100644 apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.html delete mode 100644 apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.ts diff --git a/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.html b/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.html deleted file mode 100644 index 46e1fae80df..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.html +++ /dev/null @@ -1,17 +0,0 @@ -

The Bitwarden Password Manager

-
-

- Trusted by millions of individuals, teams, and organizations worldwide for secure password - storage and sharing. -

-
-
    -
  • Store logins, secure notes, and more
  • -
  • Collaborate and share securely
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.ts deleted file mode 100644 index 0f9db7b4405..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-abm-enterprise-content", - templateUrl: "abm-enterprise-content.component.html", -}) -export class AbmEnterpriseContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.html b/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.html deleted file mode 100644 index 46e1fae80df..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.html +++ /dev/null @@ -1,17 +0,0 @@ -

The Bitwarden Password Manager

-
-

- Trusted by millions of individuals, teams, and organizations worldwide for secure password - storage and sharing. -

-
-
    -
  • Store logins, secure notes, and more
  • -
  • Collaborate and share securely
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.ts deleted file mode 100644 index 7765555f5cc..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-abm-teams-content", - templateUrl: "abm-teams-content.component.html", -}) -export class AbmTeamsContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.html b/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.html deleted file mode 100644 index b5c16911ab0..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.html +++ /dev/null @@ -1,17 +0,0 @@ -

Start Your Enterprise Free Trial Now

-
-

- Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password - storage and sharing. -

-
-
    -
  • Collaborate and share securely
  • -
  • Deploy and manage quickly and easily
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.ts deleted file mode 100644 index 4a6de8d3003..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-cnet-enterprise-content", - templateUrl: "cnet-enterprise-content.component.html", -}) -export class CnetEnterpriseContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.html b/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.html deleted file mode 100644 index 6e6f545c170..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.html +++ /dev/null @@ -1,17 +0,0 @@ -

Start Your Premium Account Now

-
-

- Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password - storage and sharing. -

-
-
    -
  • Store logins, secure notes, and more
  • -
  • Secure your account with advanced two-step login
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.ts deleted file mode 100644 index 56d8b37af90..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-cnet-individual-content", - templateUrl: "cnet-individual-content.component.html", -}) -export class CnetIndividualContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.html b/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.html deleted file mode 100644 index c719c5ac7ce..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.html +++ /dev/null @@ -1,17 +0,0 @@ -

Start Your Teams Free Trial Now

-
-

- Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password - storage and sharing. -

-
-
    -
  • Collaborate and share securely
  • -
  • Deploy and manage quickly and easily
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.ts deleted file mode 100644 index ff79a0d37cd..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-cnet-teams-content", - templateUrl: "cnet-teams-content.component.html", -}) -export class CnetTeamsContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/default-content.component.html b/apps/web/src/app/billing/trial-initiation/content/default-content.component.html deleted file mode 100644 index e1839517ff6..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/default-content.component.html +++ /dev/null @@ -1,16 +0,0 @@ -

The Bitwarden Password Manager

-
-

- Trusted by millions of individuals, teams, and organizations worldwide for secure password - storage and sharing. -

-
-
    -
  • Store logins, secure notes, and more
  • -
  • Collaborate and share securely
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/default-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/default-content.component.ts deleted file mode 100644 index 7ad40b089d1..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/default-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-default-content", - templateUrl: "default-content.component.html", -}) -export class DefaultContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.html b/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.html deleted file mode 100644 index f57fb7a3510..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.html +++ /dev/null @@ -1,44 +0,0 @@ -

Start your 7-day Enterprise free trial

-
-

- Bitwarden is the most trusted password manager designed for seamless administration and employee - usability. -

-
-
    -
  • - Instantly and securely share credentials with the groups and individuals who need them -
  • -
  • - Strengthen company-wide security through centralized administrative control and - policies -
  • -
  • - Streamline user onboarding and automate account provisioning with flexible SSO and SCIM - integrations -
  • -
  • - Migrate to Bitwarden in minutes with comprehensive import options -
  • -
  • - Give all Enterprise users the gift of 360º security with a free Families plan -
  • -
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.ts deleted file mode 100644 index 847b3c3088a..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-enterprise-content", - templateUrl: "enterprise-content.component.html", -}) -export class EnterpriseContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.html b/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.html deleted file mode 100644 index f57fb7a3510..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.html +++ /dev/null @@ -1,44 +0,0 @@ -

Start your 7-day Enterprise free trial

-
-

- Bitwarden is the most trusted password manager designed for seamless administration and employee - usability. -

-
-
    -
  • - Instantly and securely share credentials with the groups and individuals who need them -
  • -
  • - Strengthen company-wide security through centralized administrative control and - policies -
  • -
  • - Streamline user onboarding and automate account provisioning with flexible SSO and SCIM - integrations -
  • -
  • - Migrate to Bitwarden in minutes with comprehensive import options -
  • -
  • - Give all Enterprise users the gift of 360º security with a free Families plan -
  • -
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.ts deleted file mode 100644 index 7b1199eb421..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-enterprise1-content", - templateUrl: "enterprise1-content.component.html", -}) -export class Enterprise1ContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.html b/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.html deleted file mode 100644 index f57fb7a3510..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.html +++ /dev/null @@ -1,44 +0,0 @@ -

Start your 7-day Enterprise free trial

-
-

- Bitwarden is the most trusted password manager designed for seamless administration and employee - usability. -

-
-
    -
  • - Instantly and securely share credentials with the groups and individuals who need them -
  • -
  • - Strengthen company-wide security through centralized administrative control and - policies -
  • -
  • - Streamline user onboarding and automate account provisioning with flexible SSO and SCIM - integrations -
  • -
  • - Migrate to Bitwarden in minutes with comprehensive import options -
  • -
  • - Give all Enterprise users the gift of 360º security with a free Families plan -
  • -
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.ts deleted file mode 100644 index 08dec6190c7..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-enterprise2-content", - templateUrl: "enterprise2-content.component.html", -}) -export class Enterprise2ContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.html deleted file mode 100644 index d1b33eab3a4..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.html +++ /dev/null @@ -1,11 +0,0 @@ -
-
- - third party awards - -
-
diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.ts deleted file mode 100644 index c23432b67cf..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-logo-badges", - templateUrl: "logo-badges.component.html", -}) -export class LogoBadgesComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.html deleted file mode 100644 index fb4537d2820..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.html +++ /dev/null @@ -1,23 +0,0 @@ -
-
- - - - - -
-
- “Bitwarden scores points for being fully open-source, secure and audited annually by third-party - cybersecurity firms, giving it a level of transparency that sets it apart from its peers.” -
-
- - CNET Logo - -

Best Password Manager in 2024

-
-
diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.ts deleted file mode 100644 index af531829d50..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-logo-cnet-5-stars", - templateUrl: "logo-cnet-5-stars.component.html", -}) -export class LogoCnet5StarsComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.html deleted file mode 100644 index 4e04cec6da4..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.html +++ /dev/null @@ -1,15 +0,0 @@ -
-
- - CNET Logo - -
-
- "No more excuses; start using Bitwarden today. The identity you save could be your own. The - money definitely will be." -
-
diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.ts deleted file mode 100644 index 4f755f66a86..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-logo-cnet", - templateUrl: "logo-cnet.component.html", -}) -export class LogoCnetComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.html deleted file mode 100644 index 0b81e0bd216..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.html +++ /dev/null @@ -1,28 +0,0 @@ -
-

- Recommended by industry experts -

-
-
- CNET Logo - WIRED Logo -
-
- New York Times Logo - PC Mag Logo -
-
-
- “Bitwarden is currently CNET's top pick for the best password manager, thanks in part to - its commitment to transparency and its unbeatable free tier.” -
-

Best Password Manager in 2024

-
diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.ts deleted file mode 100644 index 9d9c4471820..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-logo-company-testimonial", - templateUrl: "logo-company-testimonial.component.html", -}) -export class LogoCompanyTestimonialComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.html deleted file mode 100644 index 34426168324..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.html +++ /dev/null @@ -1,15 +0,0 @@ -
-
- - Forbes Logo - -
-
- “Bitwarden boasts the backing of some of the world's best security experts and an attractive, - easy-to-use interface” -
-
diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.ts deleted file mode 100644 index 818721fd1e9..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-logo-forbes", - templateUrl: "logo-forbes.component.html", -}) -export class LogoForbesComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.html deleted file mode 100644 index bd44b56f090..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.html +++ /dev/null @@ -1,5 +0,0 @@ -US News 360 Reviews Best Password Manager diff --git a/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.ts deleted file mode 100644 index fb0b1e0c71b..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-logo-us-news", - templateUrl: "logo-us-news.component.html", -}) -export class LogoUSNewsComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.html b/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.html deleted file mode 100644 index cd719a35af8..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.html +++ /dev/null @@ -1,13 +0,0 @@ -
-

- {{ header }} -

-
- "{{ quote }}" -
-
- -

{{ source }}

-
-
-
diff --git a/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.ts b/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.ts deleted file mode 100644 index 6419ddf1e45..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.ts +++ /dev/null @@ -1,13 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, Input } from "@angular/core"; - -@Component({ - selector: "app-review-blurb", - templateUrl: "review-blurb.component.html", -}) -export class ReviewBlurbComponent { - @Input() header: string; - @Input() quote: string; - @Input() source: string; -} diff --git a/apps/web/src/app/billing/trial-initiation/content/review-logo.component.html b/apps/web/src/app/billing/trial-initiation/content/review-logo.component.html deleted file mode 100644 index 77f592f1c45..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/review-logo.component.html +++ /dev/null @@ -1,18 +0,0 @@ -
- -
-
- - - - -
-
- -
- -
-
- 4.7 -
-
diff --git a/apps/web/src/app/billing/trial-initiation/content/review-logo.component.ts b/apps/web/src/app/billing/trial-initiation/content/review-logo.component.ts deleted file mode 100644 index 9b104ac0bc3..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/review-logo.component.ts +++ /dev/null @@ -1,13 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, Input } from "@angular/core"; - -@Component({ - selector: "review-logo", - templateUrl: "review-logo.component.html", -}) -export class ReviewLogoComponent { - @Input() logoClass: string; - @Input() logoSrc: string; - @Input() logoAlt: string; -} diff --git a/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.html b/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.html deleted file mode 100644 index 569ff91f625..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.html +++ /dev/null @@ -1,30 +0,0 @@ -

{{ header }}

-
-

- {{ headline }} -

-
-
    -
  • - {{ primaryPoint }} -
  • -
-
-
-
-

{{ calloutHeadline }}

-
    -
  • - {{ callout }} -
  • -
-
-
-
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.ts deleted file mode 100644 index 955c18fddf2..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.ts +++ /dev/null @@ -1,80 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; - -@Component({ - selector: "app-secrets-manager-content", - templateUrl: "secrets-manager-content.component.html", -}) -export class SecretsManagerContentComponent implements OnInit, OnDestroy { - header: string; - headline = - "A simpler, faster way to secure and automate secrets across code and infrastructure deployments"; - primaryPoints: string[]; - calloutHeadline: string; - callouts: string[]; - - private paidPrimaryPoints = [ - "Unlimited secrets, users, and projects", - "Simple and transparent pricing", - "Zero-knowledge, end-to-end encryption", - ]; - - private paidCalloutHeadline = "Limited time offer"; - - private paidCallouts = [ - "Sign up today and receive a complimentary 12-month subscription to Bitwarden Password Manager", - "Experience complete security across your organization", - "Secure all your sensitive credentials, from user applications to machine secrets", - ]; - - private freePrimaryPoints = [ - "Unlimited secrets", - "Simple and transparent pricing", - "Zero-knowledge, end-to-end encryption", - ]; - - private freeCalloutHeadline = "Go beyond developer security!"; - - private freeCallouts = [ - "Your Bitwarden account will also grant complimentary access to Bitwarden Password Manager", - "Extend end-to-end encryption to your personal passwords, addresses, credit cards and notes", - ]; - - private destroy$ = new Subject(); - - constructor(private activatedRoute: ActivatedRoute) {} - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - - ngOnInit(): void { - this.activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((queryParameters) => { - switch (queryParameters.org) { - case "enterprise": - this.header = "Secrets Manager for Enterprise"; - this.primaryPoints = this.paidPrimaryPoints; - this.calloutHeadline = this.paidCalloutHeadline; - this.callouts = this.paidCallouts; - break; - case "free": - this.header = "Bitwarden Secrets Manager"; - this.primaryPoints = this.freePrimaryPoints; - this.calloutHeadline = this.freeCalloutHeadline; - this.callouts = this.freeCallouts; - break; - case "teams": - case "teamsStarter": - this.header = "Secrets Manager for Teams"; - this.primaryPoints = this.paidPrimaryPoints; - this.calloutHeadline = this.paidCalloutHeadline; - this.callouts = this.paidCallouts; - break; - } - }); - } -} diff --git a/apps/web/src/app/billing/trial-initiation/content/teams-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams-content.component.html deleted file mode 100644 index 46e1fae80df..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams-content.component.html +++ /dev/null @@ -1,17 +0,0 @@ -

The Bitwarden Password Manager

-
-

- Trusted by millions of individuals, teams, and organizations worldwide for secure password - storage and sharing. -

-
-
    -
  • Store logins, secure notes, and more
  • -
  • Collaborate and share securely
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/teams-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams-content.component.ts deleted file mode 100644 index 5c97695deff..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-teams-content", - templateUrl: "teams-content.component.html", -}) -export class TeamsContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.html deleted file mode 100644 index f51c370bebd..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.html +++ /dev/null @@ -1,35 +0,0 @@ -

Start your 7-day free trial for Teams

-
-

- Strengthen business security with an easy-to-use password manager your team will love. -

-
-
    -
  • - Instantly and securely share credentials with the groups and individuals who need them -
  • -
  • - Migrate to Bitwarden in minutes with comprehensive import options -
  • -
  • - Save time and increase productivity with autofill and instant device syncing -
  • -
  • - Enhance security practices across your team with easy user management -
  • -
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.ts deleted file mode 100644 index 055ec7fda10..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-teams1-content", - templateUrl: "teams1-content.component.html", -}) -export class Teams1ContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.html deleted file mode 100644 index f51c370bebd..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.html +++ /dev/null @@ -1,35 +0,0 @@ -

Start your 7-day free trial for Teams

-
-

- Strengthen business security with an easy-to-use password manager your team will love. -

-
-
    -
  • - Instantly and securely share credentials with the groups and individuals who need them -
  • -
  • - Migrate to Bitwarden in minutes with comprehensive import options -
  • -
  • - Save time and increase productivity with autofill and instant device syncing -
  • -
  • - Enhance security practices across your team with easy user management -
  • -
-
- -
diff --git a/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.ts deleted file mode 100644 index 394ba90b491..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-teams2-content", - templateUrl: "teams2-content.component.html", -}) -export class Teams2ContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.html deleted file mode 100644 index c6f1ae697ae..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.html +++ /dev/null @@ -1,26 +0,0 @@ -

Begin Teams Starter Free Trial Now

-
-

- Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password - storage and sharing. -

-
-
    -
  • - Powerful security for up to 10 users -
    - Have more than 10 users? - Start a Teams trial -
    -
  • -
  • Collaborate and share securely
  • -
  • Deploy and manage quickly and easily
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • -
-
- - -
diff --git a/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.ts deleted file mode 100644 index df91268ab26..00000000000 --- a/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "app-teams3-content", - templateUrl: "teams3-content.component.html", -}) -export class Teams3ContentComponent {} diff --git a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html deleted file mode 100644 index dddac598a46..00000000000 --- a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - -
-

{{ "smFreeTrialThankYou" | i18n }}

-
    -
  • -

    - {{ "smFreeTrialConfirmationEmail" | i18n }} - {{ formGroup.get("email").value }}. -

    -
  • -
-
-
- - -
-
-
diff --git a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.ts b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.ts deleted file mode 100644 index f7c5a9b2b98..00000000000 --- a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.ts +++ /dev/null @@ -1,90 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnInit, ViewChild } from "@angular/core"; -import { UntypedFormBuilder, Validators } from "@angular/forms"; -import { Router } from "@angular/router"; - -import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service"; -import { PlanType } from "@bitwarden/common/billing/enums"; -import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; - -import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component"; - -@Component({ - selector: "app-secrets-manager-trial-free-stepper", - templateUrl: "secrets-manager-trial-free-stepper.component.html", -}) -export class SecretsManagerTrialFreeStepperComponent implements OnInit { - @ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent; - - formGroup = this.formBuilder.group({ - name: [ - "", - { - validators: [Validators.required, Validators.maxLength(50)], - updateOn: "change", - }, - ], - email: [ - "", - { - validators: [Validators.email], - }, - ], - }); - - subLabels = { - createAccount: - "Before creating your free organization, you first need to log in or create a personal account.", - organizationInfo: "Enter your organization information", - }; - - organizationId: string; - - referenceEventRequest: ReferenceEventRequest; - - constructor( - protected formBuilder: UntypedFormBuilder, - protected i18nService: I18nService, - protected organizationBillingService: OrganizationBillingService, - protected router: Router, - ) {} - - ngOnInit(): void { - this.referenceEventRequest = new ReferenceEventRequest(); - this.referenceEventRequest.initiationPath = "Secrets Manager trial from marketing website"; - } - - accountCreated(email: string): void { - this.formGroup.get("email")?.setValue(email); - this.subLabels.createAccount = email; - this.verticalStepper.next(); - } - - async createOrganization(): Promise { - const response = await this.organizationBillingService.startFree({ - organization: { - name: this.formGroup.get("name").value, - billingEmail: this.formGroup.get("email").value, - }, - plan: { - type: PlanType.Free, - subscribeToSecretsManager: true, - isFromSecretsManagerTrial: true, - }, - }); - - this.organizationId = response.id; - this.subLabels.organizationInfo = response.name; - this.verticalStepper.next(); - } - - async navigateToMembers(): Promise { - await this.router.navigate(["organizations", this.organizationId, "members"]); - } - - async navigateToSecretsManager(): Promise { - await this.router.navigate(["sm", this.organizationId]); - } -} diff --git a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.html b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.html deleted file mode 100644 index 99e2706d713..00000000000 --- a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - -
- - -
-
-
diff --git a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.ts b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.ts deleted file mode 100644 index 650c1d8e69e..00000000000 --- a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component.ts +++ /dev/null @@ -1,144 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, Input, OnInit, ViewChild } from "@angular/core"; -import { UntypedFormBuilder } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; - -import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service"; -import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; - -import { - OrganizationCreatedEvent, - SubscriptionProduct, - TrialOrganizationType, -} from "../../../billing/accounts/trial-initiation/trial-billing-step.component"; -import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component"; -import { SecretsManagerTrialFreeStepperComponent } from "../secrets-manager/secrets-manager-trial-free-stepper.component"; - -export enum ValidOrgParams { - families = "families", - enterprise = "enterprise", - teams = "teams", - teamsStarter = "teamsStarter", - individual = "individual", - premium = "premium", - free = "free", -} - -const trialFlowOrgs = [ - ValidOrgParams.teams, - ValidOrgParams.teamsStarter, - ValidOrgParams.enterprise, - ValidOrgParams.families, -]; - -@Component({ - selector: "app-secrets-manager-trial-paid-stepper", - templateUrl: "secrets-manager-trial-paid-stepper.component.html", -}) -export class SecretsManagerTrialPaidStepperComponent - extends SecretsManagerTrialFreeStepperComponent - implements OnInit -{ - @ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent; - @Input() organizationTypeQueryParameter: string; - - plan: PlanType; - createOrganizationLoading = false; - billingSubLabel = this.i18nService.t("billingTrialSubLabel"); - organizationId: string; - - private destroy$ = new Subject(); - protected enableTrialPayment$ = this.configService.getFeatureFlag$( - FeatureFlag.TrialPaymentOptional, - ); - - constructor( - private route: ActivatedRoute, - private configService: ConfigService, - protected formBuilder: UntypedFormBuilder, - protected i18nService: I18nService, - protected organizationBillingService: OrganizationBillingService, - protected router: Router, - ) { - super(formBuilder, i18nService, organizationBillingService, router); - } - - async ngOnInit(): Promise { - this.referenceEventRequest = new ReferenceEventRequest(); - this.referenceEventRequest.initiationPath = "Secrets Manager trial from marketing website"; - - this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((qParams) => { - if (trialFlowOrgs.includes(qParams.org)) { - if (qParams.org === ValidOrgParams.teamsStarter) { - this.plan = PlanType.TeamsStarter; - } else if (qParams.org === ValidOrgParams.teams) { - this.plan = PlanType.TeamsAnnually; - } else if (qParams.org === ValidOrgParams.enterprise) { - this.plan = PlanType.EnterpriseAnnually; - } - } - }); - } - - organizationCreated(event: OrganizationCreatedEvent) { - this.organizationId = event.organizationId; - this.billingSubLabel = event.planDescription; - this.verticalStepper.next(); - } - - steppedBack() { - this.verticalStepper.previous(); - } - - async createOrganizationOnTrial(): Promise { - this.createOrganizationLoading = true; - const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({ - organization: { - name: this.formGroup.get("name").value, - billingEmail: this.formGroup.get("email").value, - initiationPath: "Secrets Manager trial from marketing website", - }, - plan: { - type: this.plan, - subscribeToSecretsManager: true, - isFromSecretsManagerTrial: true, - passwordManagerSeats: 1, - secretsManagerSeats: 1, - }, - }); - - this.organizationId = response?.id; - this.subLabels.organizationInfo = response?.name; - this.createOrganizationLoading = false; - this.verticalStepper.next(); - } - - get createAccountLabel() { - const organizationType = - this.productType === ProductTierType.TeamsStarter - ? "Teams Starter" - : ProductTierType[this.productType]; - return `Before creating your ${organizationType} organization, you first need to log in or create a personal account.`; - } - - get productType(): TrialOrganizationType { - switch (this.organizationTypeQueryParameter) { - case "enterprise": - return ProductTierType.Enterprise; - case "families": - return ProductTierType.Families; - case "teams": - return ProductTierType.Teams; - case "teamsStarter": - return ProductTierType.TeamsStarter; - } - } - - protected readonly SubscriptionProduct = SubscriptionProduct; -} diff --git a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.html b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.html deleted file mode 100644 index 88251136dbe..00000000000 --- a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.html +++ /dev/null @@ -1,44 +0,0 @@ - - -
-
-
- Bitwarden -
- -
-
-
-
-
-
-

- {{ - "startYour7DayFreeTrialOfBitwardenSecretsManagerFor" - | i18n: organizationTypeQueryParameter - }} -

- -
- - -
-
-
-
-
diff --git a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.ts b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.ts deleted file mode 100644 index 678514532ca..00000000000 --- a/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial.component.ts +++ /dev/null @@ -1,32 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; - -@Component({ - selector: "app-secrets-manager-trial", - templateUrl: "secrets-manager-trial.component.html", -}) -export class SecretsManagerTrialComponent implements OnInit, OnDestroy { - organizationTypeQueryParameter: string; - - private destroy$ = new Subject(); - - constructor(private route: ActivatedRoute) {} - - ngOnInit(): void { - this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((queryParameters) => { - this.organizationTypeQueryParameter = queryParameters.org; - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - get freeOrganization() { - return this.organizationTypeQueryParameter === "free"; - } -} diff --git a/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts b/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts index 3e6bfdc4e6c..06e1cce7f23 100644 --- a/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts +++ b/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts @@ -7,36 +7,10 @@ import { FormFieldModule } from "@bitwarden/components"; import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module"; import { TrialBillingStepComponent } from "../../billing/accounts/trial-initiation/trial-billing-step.component"; -import { SecretsManagerTrialFreeStepperComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component"; -import { SecretsManagerTrialPaidStepperComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component"; -import { SecretsManagerTrialComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial.component"; -import { EnvironmentSelectorModule } from "../../components/environment-selector/environment-selector.module"; import { SharedModule } from "../../shared"; import { CompleteTrialInitiationComponent } from "./complete-trial-initiation/complete-trial-initiation.component"; import { ConfirmationDetailsComponent } from "./confirmation-details.component"; -import { AbmEnterpriseContentComponent } from "./content/abm-enterprise-content.component"; -import { AbmTeamsContentComponent } from "./content/abm-teams-content.component"; -import { CnetEnterpriseContentComponent } from "./content/cnet-enterprise-content.component"; -import { CnetIndividualContentComponent } from "./content/cnet-individual-content.component"; -import { CnetTeamsContentComponent } from "./content/cnet-teams-content.component"; -import { DefaultContentComponent } from "./content/default-content.component"; -import { EnterpriseContentComponent } from "./content/enterprise-content.component"; -import { Enterprise1ContentComponent } from "./content/enterprise1-content.component"; -import { Enterprise2ContentComponent } from "./content/enterprise2-content.component"; -import { LogoBadgesComponent } from "./content/logo-badges.component"; -import { LogoCnet5StarsComponent } from "./content/logo-cnet-5-stars.component"; -import { LogoCnetComponent } from "./content/logo-cnet.component"; -import { LogoCompanyTestimonialComponent } from "./content/logo-company-testimonial.component"; -import { LogoForbesComponent } from "./content/logo-forbes.component"; -import { LogoUSNewsComponent } from "./content/logo-us-news.component"; -import { ReviewBlurbComponent } from "./content/review-blurb.component"; -import { ReviewLogoComponent } from "./content/review-logo.component"; -import { SecretsManagerContentComponent } from "./content/secrets-manager-content.component"; -import { TeamsContentComponent } from "./content/teams-content.component"; -import { Teams1ContentComponent } from "./content/teams1-content.component"; -import { Teams2ContentComponent } from "./content/teams2-content.component"; -import { Teams3ContentComponent } from "./content/teams3-content.component"; import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.module"; @NgModule({ @@ -46,41 +20,10 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul VerticalStepperModule, FormFieldModule, OrganizationCreateModule, - EnvironmentSelectorModule, TrialBillingStepComponent, InputPasswordComponent, ], - declarations: [ - CompleteTrialInitiationComponent, - EnterpriseContentComponent, - TeamsContentComponent, - ConfirmationDetailsComponent, - DefaultContentComponent, - EnterpriseContentComponent, - Enterprise1ContentComponent, - Enterprise2ContentComponent, - TeamsContentComponent, - Teams1ContentComponent, - Teams2ContentComponent, - Teams3ContentComponent, - CnetEnterpriseContentComponent, - CnetIndividualContentComponent, - CnetTeamsContentComponent, - AbmEnterpriseContentComponent, - AbmTeamsContentComponent, - LogoBadgesComponent, - LogoCnet5StarsComponent, - LogoCompanyTestimonialComponent, - LogoCnetComponent, - LogoForbesComponent, - LogoUSNewsComponent, - ReviewLogoComponent, - SecretsManagerContentComponent, - ReviewBlurbComponent, - SecretsManagerTrialComponent, - SecretsManagerTrialFreeStepperComponent, - SecretsManagerTrialPaidStepperComponent, - ], + declarations: [CompleteTrialInitiationComponent, ConfirmationDetailsComponent], exports: [CompleteTrialInitiationComponent], providers: [TitleCasePipe], }) From ef80c2370700c79a095d562bbc4cb1ab03d49a8f Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 23 Apr 2025 18:45:29 +0200 Subject: [PATCH 5/8] Fix type 0 not being blocked on key wrapping (#14388) * Fix type 0 not being blocked on key wrapping * Move block type0 below key null check --- .../encrypt.service.implementation.ts | 6 +++ .../crypto/services/encrypt.service.spec.ts | 39 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts index f4318840515..fceef34421c 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts @@ -116,6 +116,12 @@ export class EncryptServiceImplementation implements EncryptService { throw new Error("No encryption key provided."); } + if (this.blockType0) { + if (key.inner().type === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { + throw new Error("Type 0 encryption is not supported."); + } + } + if (plainValue == null) { return Promise.resolve(null); } diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts index 6b2851ad116..bc945a5eff7 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts @@ -55,6 +55,19 @@ describe("EncryptService", () => { "No wrappingKey provided for wrapping.", ); }); + it("fails if type 0 key is provided with flag turned on", async () => { + (encryptService as any).blockType0 = true; + const mock32Key = mock(); + mock32Key.key = makeStaticByteArray(32); + mock32Key.inner.mockReturnValue({ + type: 0, + encryptionKey: mock32Key.key, + }); + + await expect(encryptService.wrapSymmetricKey(mock32Key, mock32Key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + }); }); describe("wrapDecapsulationKey", () => { @@ -83,6 +96,19 @@ describe("EncryptService", () => { "No wrappingKey provided for wrapping.", ); }); + it("throws if type 0 key is provided with flag turned on", async () => { + (encryptService as any).blockType0 = true; + const mock32Key = mock(); + mock32Key.key = makeStaticByteArray(32); + mock32Key.inner.mockReturnValue({ + type: 0, + encryptionKey: mock32Key.key, + }); + + await expect( + encryptService.wrapDecapsulationKey(new Uint8Array(200), mock32Key), + ).rejects.toThrow("Type 0 encryption is not supported."); + }); }); describe("wrapEncapsulationKey", () => { @@ -111,6 +137,19 @@ describe("EncryptService", () => { "No wrappingKey provided for wrapping.", ); }); + it("throws if type 0 key is provided with flag turned on", async () => { + (encryptService as any).blockType0 = true; + const mock32Key = mock(); + mock32Key.key = makeStaticByteArray(32); + mock32Key.inner.mockReturnValue({ + type: 0, + encryptionKey: mock32Key.key, + }); + + await expect( + encryptService.wrapEncapsulationKey(new Uint8Array(200), mock32Key), + ).rejects.toThrow("Type 0 encryption is not supported."); + }); }); describe("onServerConfigChange", () => { From b589951c907eb23cc0e2c0711979e3619e1d1e59 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 23 Apr 2025 11:13:44 -0700 Subject: [PATCH 6/8] [PM-18520] - Update desktop cipher forms to use the same UI as web app and extension - (#13992) * WIP - cipher form refactor * cipher clone * cipher clone * finalize item view and form changes * fix tests * hide changes behind feature flag * set flag to false * create vault items v2. add button selector * revert change to flag and vault items * add attachments * revert change to tsconfig * move module * fix modules * cleanup * fix import * fix import * fix import * remove showForm * update feature flag * wip - cleanup * fix up services * cleanup * fix type errors * fix lint errors * add dialog component * revert changes to menu * revert changes to menu * fix vault-items-v2 * set feature flag to FALSE * add missing i18n keys. fix collection state * remove generator. update modules. bug fix * fix restricted imports * mark method as deprecated. add uri arg back * fix shared.module * fix shared.module * fix shared.module * add uri * check and prompt for premium when opening attachments dialog * move VaultItemDialogResult back * fix import in spec file * update copy functions * fix MP reprompt issue --- .../browser-view-password-history.service.ts | 2 - apps/desktop/src/app/app-routing.module.ts | 19 +- apps/desktop/src/app/app.module.ts | 8 +- apps/desktop/src/locales/en/messages.json | 142 ++++ apps/desktop/src/scss/base.scss | 5 + apps/desktop/src/scss/vault.scss | 4 + .../desktop-cipher-form-generator.service.ts | 32 + ...top-premium-upgrade-prompt.service.spec.ts | 30 + .../desktop-premium-upgrade-prompt.service.ts | 15 + ...credential-generator-dialog.component.html | 2 +- .../credential-generator-dialog.component.ts | 37 +- .../app/vault/item-footer.component.html | 64 ++ .../vault/app/vault/item-footer.component.ts | 159 ++++ .../app/vault/vault-items-v2.component.html | 92 ++ .../app/vault/vault-items-v2.component.ts | 42 + .../vault/app/vault/vault-v2.component.html | 80 ++ .../src/vault/app/vault/vault-v2.component.ts | 785 ++++++++++++++++++ .../collections/vault.component.ts | 6 +- .../view/emergency-view-dialog.component.ts | 5 +- .../vault-item-dialog.component.ts | 14 +- .../individual-vault/add-edit-v2.component.ts | 3 +- .../vault/individual-vault/vault.component.ts | 8 +- .../vault/individual-vault/view.component.ts | 8 +- .../view-password-history.service.spec.ts | 15 +- .../services/view-password-history.service.ts | 7 +- .../vault/components/vault-items.component.ts | 8 +- libs/common/src/enums/feature-flag.enum.ts | 2 + .../components/identity/identity.component.ts | 32 +- .../attachments-v2.component.html | 0 .../attachments-v2.component.spec.ts | 0 .../attachments}/attachments-v2.component.ts | 14 +- .../src/cipher-view/cipher-view.component.ts | 3 +- libs/vault/src/cipher-view/index.ts | 1 + .../password-history.component.html | 0 .../password-history.component.ts | 13 +- libs/vault/src/index.ts | 1 + 36 files changed, 1569 insertions(+), 89 deletions(-) create mode 100644 apps/desktop/src/services/desktop-cipher-form-generator.service.ts create mode 100644 apps/desktop/src/services/desktop-premium-upgrade-prompt.service.spec.ts create mode 100644 apps/desktop/src/services/desktop-premium-upgrade-prompt.service.ts create mode 100644 apps/desktop/src/vault/app/vault/item-footer.component.html create mode 100644 apps/desktop/src/vault/app/vault/item-footer.component.ts create mode 100644 apps/desktop/src/vault/app/vault/vault-items-v2.component.html create mode 100644 apps/desktop/src/vault/app/vault/vault-items-v2.component.ts create mode 100644 apps/desktop/src/vault/app/vault/vault-v2.component.html create mode 100644 apps/desktop/src/vault/app/vault/vault-v2.component.ts rename apps/web/src/app/vault/services/web-view-password-history.service.spec.ts => libs/angular/src/services/view-password-history.service.spec.ts (69%) rename apps/web/src/app/vault/services/web-view-password-history.service.ts => libs/angular/src/services/view-password-history.service.ts (78%) rename {apps/web/src/app/vault/individual-vault => libs/vault/src/cipher-view/attachments}/attachments-v2.component.html (100%) rename {apps/web/src/app/vault/individual-vault => libs/vault/src/cipher-view/attachments}/attachments-v2.component.spec.ts (100%) rename {apps/web/src/app/vault/individual-vault => libs/vault/src/cipher-view/attachments}/attachments-v2.component.ts (84%) rename {apps/web/src/app/vault/individual-vault => libs/vault/src/components/password-history}/password-history.component.html (100%) rename {apps/web/src/app/vault/individual-vault => libs/vault/src/components/password-history}/password-history.component.ts (95%) diff --git a/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts b/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts index 5e400da9de5..ae6369d06a5 100644 --- a/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts +++ b/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { inject } from "@angular/core"; import { Router } from "@angular/router"; diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 0c6bc730c2c..00463152a95 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -14,6 +14,7 @@ import { tdeDecryptionRequiredGuard, unauthGuardFn, } from "@bitwarden/angular/auth/guards"; +import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, @@ -41,6 +42,7 @@ import { NewDeviceVerificationComponent, DeviceVerificationIcon, } from "@bitwarden/auth/angular"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { LockComponent } from "@bitwarden/key-management-ui"; import { NewDeviceVerificationNoticePageOneComponent, @@ -53,6 +55,7 @@ import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; import { SetPasswordComponent } from "../auth/set-password.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; +import { VaultV2Component } from "../vault/app/vault/vault-v2.component"; import { VaultComponent } from "../vault/app/vault/vault.component"; import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component"; @@ -132,11 +135,15 @@ const routes: Routes = [ }, ], }, - { - path: "vault", - component: VaultComponent, - canActivate: [authGuard, NewDeviceVerificationNoticeGuard], - }, + ...featureFlaggedRoute({ + defaultComponent: VaultComponent, + flaggedComponent: VaultV2Component, + featureFlag: FeatureFlag.PM18520_UpdateDesktopCipherForm, + routeOptions: { + path: "vault", + canActivate: [authGuard, NewDeviceVerificationNoticeGuard], + }, + }), { path: "accessibility-cookie", component: AccessibilityCookieComponent }, { path: "set-password", component: SetPasswordComponent }, { @@ -359,7 +366,7 @@ const routes: Routes = [ imports: [ RouterModule.forRoot(routes, { useHash: true, - /*enableTracing: true,*/ + // enableTracing: true, }), ], exports: [RouterModule], diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index c84f1a96afd..15ab4350bbc 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -9,7 +9,6 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe"; import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe"; import { CalloutModule, DialogModule } from "@bitwarden/components"; -import { DecryptionFailureDialogComponent } from "@bitwarden/vault"; import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; import { DeleteAccountComponent } from "../auth/delete-account.component"; @@ -28,6 +27,7 @@ import { PasswordHistoryComponent } from "../vault/app/vault/password-history.co import { ShareComponent } from "../vault/app/vault/share.component"; import { VaultFilterModule } from "../vault/app/vault/vault-filter/vault-filter.module"; import { VaultItemsComponent } from "../vault/app/vault/vault-items.component"; +import { VaultV2Component } from "../vault/app/vault/vault-v2.component"; import { VaultComponent } from "../vault/app/vault/vault.component"; import { ViewCustomFieldsComponent } from "../vault/app/vault/view-custom-fields.component"; import { ViewComponent } from "../vault/app/vault/view.component"; @@ -55,8 +55,8 @@ import { SharedModule } from "./shared/shared.module"; CalloutModule, DeleteAccountComponent, UserVerificationComponent, - DecryptionFailureDialogComponent, NavComponent, + VaultV2Component, ], declarations: [ AccessibilityCookieComponent, @@ -65,7 +65,6 @@ import { SharedModule } from "./shared/shared.module"; AddEditCustomFieldsComponent, AppComponent, AttachmentsComponent, - VaultItemsComponent, CollectionsComponent, ColorPasswordPipe, ColorPasswordCountPipe, @@ -80,9 +79,10 @@ import { SharedModule } from "./shared/shared.module"; ShareComponent, UpdateTempPasswordComponent, VaultComponent, + VaultItemsComponent, VaultTimeoutInputComponent, - ViewComponent, ViewCustomFieldsComponent, + ViewComponent, ], providers: [SshAgentService], bootstrap: [AppComponent], diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 6097543e50a..81e3a94ff4d 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -393,6 +393,64 @@ "authenticatorKeyTotp": { "message": "Authenticator key (TOTP)" }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "message": "Autofill options" + }, + "websiteUri": { + "message": "Website (URI)" + }, + "websiteUriCount": { + "message": "Website (URI) $COUNT$", + "description": "Label for an input field that contains a website URI. The input field is part of a list of fields, and the count indicates the position of the field in the list.", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "websiteAdded": { + "message": "Website added" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "owner": { + "message": "Owner" + }, + "addField": { + "message": "Add field" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "add": { + "message": "Add" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, "folder": { "message": "Folder" }, @@ -418,6 +476,9 @@ "message": "Linked", "description": "This describes a field that is 'linked' (related) to another field." }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "linkedValue": { "message": "Linked value", "description": "This describes a value that is 'linked' (related) to another value." @@ -1915,6 +1976,43 @@ } } }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "learnMoreAboutAuthenticators": { + "message": "Learn more about authenticators" + }, + "copyTOTP": { + "message": "Copy Authenticator key (TOTP)" + }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "totpHelperWithCapture": { + "message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field." + }, + "premium": { + "message": "Premium", + "description": "Premium membership" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2084,6 +2182,15 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact information" + }, "allSends": { "message": "All Sends", "description": "'Sends' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2518,6 +2625,9 @@ "generateEmail": { "message": "Generate email" }, + "usernameGenerator": { + "message": "Username generator" + }, "spinboxBoundariesHint": { "message": "Value must be between $MIN$ and $MAX$.", "description": "Explains spin box minimum and maximum values to the user", @@ -3439,6 +3549,17 @@ "ssoError": { "message": "No free ports could be found for the sso login." }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "biometricsStatusHelptextUnlockNeeded": { "message": "Biometric unlock is unavailable because PIN or password unlock is required first." }, @@ -3517,6 +3638,27 @@ "setupTwoStepLogin": { "message": "Set up two-step login" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "loginCredentials": { + "message": "Login credentials" + }, + "additionalOptions": { + "message": "Additional options" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "upload": { + "message": "Upload" + }, "newDeviceVerificationNoticeContentPage1": { "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." }, diff --git a/apps/desktop/src/scss/base.scss b/apps/desktop/src/scss/base.scss index 22eb3df0d17..494e91529ee 100644 --- a/apps/desktop/src/scss/base.scss +++ b/apps/desktop/src/scss/base.scss @@ -147,3 +147,8 @@ div:not(.modal)::-webkit-scrollbar-thumb, .mx-auto { margin-left: auto !important; } + +.vault-v2 button:not([bitbutton]):not([biticonbutton]) i.bwi, +a i.bwi { + margin-right: 0.25rem; +} diff --git a/apps/desktop/src/scss/vault.scss b/apps/desktop/src/scss/vault.scss index f7403ad62d2..88216a2b926 100644 --- a/apps/desktop/src/scss/vault.scss +++ b/apps/desktop/src/scss/vault.scss @@ -162,3 +162,7 @@ app-root { } } } + +.vault-v2 > .details { + flex-direction: column-reverse; +} diff --git a/apps/desktop/src/services/desktop-cipher-form-generator.service.ts b/apps/desktop/src/services/desktop-cipher-form-generator.service.ts new file mode 100644 index 00000000000..8a33f4ced0a --- /dev/null +++ b/apps/desktop/src/services/desktop-cipher-form-generator.service.ts @@ -0,0 +1,32 @@ +import { inject, Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { DialogService } from "@bitwarden/components"; +import { CipherFormGenerationService } from "@bitwarden/vault"; + +import { CredentialGeneratorDialogComponent } from "../vault/app/vault/credential-generator-dialog.component"; + +@Injectable() +export class DesktopCredentialGenerationService implements CipherFormGenerationService { + private dialogService = inject(DialogService); + + async generatePassword(): Promise { + return await this.generateCredential("password"); + } + + async generateUsername(uri: string): Promise { + return await this.generateCredential("username", uri); + } + + async generateCredential(type: "password" | "username", uri?: string): Promise { + const dialogRef = CredentialGeneratorDialogComponent.open(this.dialogService, { type, uri }); + + const result = await firstValueFrom(dialogRef.closed); + + if (!result || result.action === "canceled" || !result.generatedValue) { + return ""; + } + + return result.generatedValue; + } +} diff --git a/apps/desktop/src/services/desktop-premium-upgrade-prompt.service.spec.ts b/apps/desktop/src/services/desktop-premium-upgrade-prompt.service.spec.ts new file mode 100644 index 00000000000..3b33116ea5a --- /dev/null +++ b/apps/desktop/src/services/desktop-premium-upgrade-prompt.service.spec.ts @@ -0,0 +1,30 @@ +import { TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; + +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; + +import { DesktopPremiumUpgradePromptService } from "./desktop-premium-upgrade-prompt.service"; + +describe("DesktopPremiumUpgradePromptService", () => { + let service: DesktopPremiumUpgradePromptService; + let messager: MockProxy; + + beforeEach(async () => { + messager = mock(); + await TestBed.configureTestingModule({ + providers: [ + DesktopPremiumUpgradePromptService, + { provide: MessagingService, useValue: messager }, + ], + }).compileComponents(); + + service = TestBed.inject(DesktopPremiumUpgradePromptService); + }); + + describe("promptForPremium", () => { + it("navigates to the premium update screen", async () => { + await service.promptForPremium(); + expect(messager.send).toHaveBeenCalledWith("openPremium"); + }); + }); +}); diff --git a/apps/desktop/src/services/desktop-premium-upgrade-prompt.service.ts b/apps/desktop/src/services/desktop-premium-upgrade-prompt.service.ts new file mode 100644 index 00000000000..f2375ecfebb --- /dev/null +++ b/apps/desktop/src/services/desktop-premium-upgrade-prompt.service.ts @@ -0,0 +1,15 @@ +import { inject } from "@angular/core"; + +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; + +/** + * This class handles the premium upgrade process for the desktop. + */ +export class DesktopPremiumUpgradePromptService implements PremiumUpgradePromptService { + private messagingService = inject(MessagingService); + + async promptForPremium() { + this.messagingService.send("openPremium"); + } +} diff --git a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html index 47232dff66d..31f47d824d6 100644 --- a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html +++ b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html @@ -3,6 +3,7 @@ @@ -27,7 +28,6 @@ (click)="applyCredentials()" appA11yTitle="{{ buttonLabel }}" bitButton - bitDialogClose [disabled]="!(buttonLabel && credentialValue)" > {{ buttonLabel }} diff --git a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts index eda35a8c76d..2858d7330e5 100644 --- a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts +++ b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts @@ -10,6 +10,7 @@ import { DialogService, ItemModule, LinkModule, + DialogRef, } from "@bitwarden/components"; import { CredentialGeneratorHistoryDialogComponent, @@ -19,10 +20,22 @@ import { AlgorithmInfo } from "@bitwarden/generator-core"; import { CipherFormGeneratorComponent } from "@bitwarden/vault"; type CredentialGeneratorParams = { - onCredentialGenerated: (value?: string) => void; + /** @deprecated Prefer use of dialogRef.closed to retreive the generated value */ + onCredentialGenerated?: (value?: string) => void; type: "password" | "username"; + uri?: string; }; +export interface CredentialGeneratorDialogResult { + action: CredentialGeneratorDialogAction; + generatedValue?: string; +} + +export enum CredentialGeneratorDialogAction { + Selected = "selected", + Canceled = "canceled", +} + @Component({ standalone: true, selector: "credential-generator-dialog", @@ -45,6 +58,7 @@ export class CredentialGeneratorDialogComponent { constructor( @Inject(DIALOG_DATA) protected data: CredentialGeneratorParams, private dialogService: DialogService, + private dialogRef: DialogRef, private i18nService: I18nService, ) {} @@ -59,11 +73,15 @@ export class CredentialGeneratorDialogComponent { }; applyCredentials = () => { - this.data.onCredentialGenerated(this.credentialValue); + this.data.onCredentialGenerated?.(this.credentialValue); + this.dialogRef.close({ + action: CredentialGeneratorDialogAction.Selected, + generatedValue: this.credentialValue, + }); }; clearCredentials = () => { - this.data.onCredentialGenerated(); + this.data.onCredentialGenerated?.(); }; onCredentialGenerated = (value: string) => { @@ -75,9 +93,12 @@ export class CredentialGeneratorDialogComponent { this.dialogService.open(CredentialGeneratorHistoryDialogComponent); }; - static open = (dialogService: DialogService, data: CredentialGeneratorParams) => { - dialogService.open(CredentialGeneratorDialogComponent, { - data, - }); - }; + static open(dialogService: DialogService, data: CredentialGeneratorParams) { + return dialogService.open( + CredentialGeneratorDialogComponent, + { + data, + }, + ); + } } diff --git a/apps/desktop/src/vault/app/vault/item-footer.component.html b/apps/desktop/src/vault/app/vault/item-footer.component.html new file mode 100644 index 00000000000..6915555c08b --- /dev/null +++ b/apps/desktop/src/vault/app/vault/item-footer.component.html @@ -0,0 +1,64 @@ + diff --git a/apps/desktop/src/vault/app/vault/item-footer.component.ts b/apps/desktop/src/vault/app/vault/item-footer.component.ts new file mode 100644 index 00000000000..639d1557ecd --- /dev/null +++ b/apps/desktop/src/vault/app/vault/item-footer.component.ts @@ -0,0 +1,159 @@ +import { CommonModule } from "@angular/common"; +import { Input, Output, EventEmitter, Component, OnInit, ViewChild } from "@angular/core"; +import { Observable, firstValueFrom } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CollectionId, UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { ButtonComponent, ButtonModule, DialogService, ToastService } from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +@Component({ + selector: "app-vault-item-footer", + templateUrl: "item-footer.component.html", + standalone: true, + imports: [ButtonModule, CommonModule, JslibModule], +}) +export class ItemFooterComponent implements OnInit { + @Input({ required: true }) cipher: CipherView = new CipherView(); + @Input() collectionId: string | null = null; + @Input({ required: true }) action: string = "view"; + @Input() isSubmitting: boolean = false; + @Output() onEdit = new EventEmitter(); + @Output() onClone = new EventEmitter(); + @Output() onDelete = new EventEmitter(); + @Output() onRestore = new EventEmitter(); + @Output() onCancel = new EventEmitter(); + @ViewChild("submitBtn", { static: false }) submitBtn: ButtonComponent | null = null; + + canDeleteCipher$: Observable = new Observable(); + activeUserId: UserId | null = null; + + private passwordReprompted = false; + + constructor( + protected cipherService: CipherService, + protected dialogService: DialogService, + protected passwordRepromptService: PasswordRepromptService, + protected cipherAuthorizationService: CipherAuthorizationService, + protected accountService: AccountService, + protected toastService: ToastService, + protected i18nService: I18nService, + protected logService: LogService, + ) {} + + async ngOnInit() { + this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [ + this.collectionId as CollectionId, + ]); + this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + } + + async clone() { + if (this.cipher.login?.hasFido2Credentials) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "passkeyNotCopied" }, + content: { key: "passkeyNotCopiedAlert" }, + type: "info", + }); + + if (!confirmed) { + return false; + } + } + + if (await this.promptPassword()) { + this.onClone.emit(this.cipher); + return true; + } + + return false; + } + + protected edit() { + this.onEdit.emit(this.cipher); + } + + cancel() { + this.onCancel.emit(this.cipher); + } + + async delete(): Promise { + if (!(await this.promptPassword())) { + return false; + } + + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteItem" }, + content: { + key: this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation", + }, + type: "warning", + }); + + if (!confirmed) { + return false; + } + + try { + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.deleteCipher(activeUserId); + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t( + this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem", + ), + }); + this.onDelete.emit(this.cipher); + } catch (e) { + this.logService.error(e); + } + + return true; + } + + async restore(): Promise { + if (!this.cipher.isDeleted) { + return false; + } + + try { + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.restoreCipher(activeUserId); + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t("restoredItem"), + }); + this.onRestore.emit(this.cipher); + } catch (e) { + this.logService.error(e); + } + + return true; + } + + protected deleteCipher(userId: UserId) { + return this.cipher.isDeleted + ? this.cipherService.deleteWithServer(this.cipher.id, userId) + : this.cipherService.softDeleteWithServer(this.cipher.id, userId); + } + + protected restoreCipher(userId: UserId) { + return this.cipherService.restoreWithServer(this.cipher.id, userId); + } + + protected async promptPassword() { + if (this.cipher.reprompt === CipherRepromptType.None || this.passwordReprompted) { + return true; + } + + return (this.passwordReprompted = await this.passwordRepromptService.showPasswordPrompt()); + } +} diff --git a/apps/desktop/src/vault/app/vault/vault-items-v2.component.html b/apps/desktop/src/vault/app/vault/vault-items-v2.component.html new file mode 100644 index 00000000000..ff35e00fb0f --- /dev/null +++ b/apps/desktop/src/vault/app/vault/vault-items-v2.component.html @@ -0,0 +1,92 @@ +
+ +
+ +
+ +
+ +
+
+
+ +

{{ "noItemsInList" | i18n }}

+ +
+ +
+
+ + + + + + + + + + diff --git a/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts new file mode 100644 index 00000000000..31d4098d2b2 --- /dev/null +++ b/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts @@ -0,0 +1,42 @@ +import { ScrollingModule } from "@angular/cdk/scrolling"; +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { distinctUntilChanged } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/vault/components/vault-items.component"; +import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { MenuModule } from "@bitwarden/components"; + +import { SearchBarService } from "../../../app/layout/search/search-bar.service"; + +@Component({ + selector: "app-vault-items-v2", + templateUrl: "vault-items-v2.component.html", + standalone: true, + imports: [MenuModule, CommonModule, JslibModule, ScrollingModule], +}) +export class VaultItemsV2Component extends BaseVaultItemsComponent { + constructor( + searchService: SearchService, + private readonly searchBarService: SearchBarService, + cipherService: CipherService, + accountService: AccountService, + ) { + super(searchService, cipherService, accountService); + + this.searchBarService.searchText$ + .pipe(distinctUntilChanged(), takeUntilDestroyed()) + .subscribe((searchText) => { + this.searchText = searchText!; + }); + } + + trackByFn(index: number, c: CipherView): string { + return c.id; + } +} diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.html b/apps/desktop/src/vault/app/vault/vault-v2.component.html new file mode 100644 index 00000000000..12f52502984 --- /dev/null +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.html @@ -0,0 +1,80 @@ +
+ + +
+ +
+
+
+ + + + + + + +
+
+
+
+ +
+ + +
+
+ diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-v2.component.ts new file mode 100644 index 00000000000..7e799899418 --- /dev/null +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.ts @@ -0,0 +1,785 @@ +import { CommonModule } from "@angular/common"; +import { + ChangeDetectorRef, + Component, + NgZone, + OnDestroy, + OnInit, + ViewChild, + ViewContainerRef, +} from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom, Subject, takeUntil, switchMap } from "rxjs"; +import { filter, map, take } from "rxjs/operators"; + +import { CollectionView } from "@bitwarden/admin-console/common"; +import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; +import { ModalService } from "@bitwarden/angular/services/modal.service"; +import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service"; +import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { EventType } from "@bitwarden/common/enums"; +import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; +import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; +import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { + BadgeModule, + ButtonModule, + DialogService, + ItemModule, + ToastService, +} from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { + AttachmentDialogResult, + AttachmentsV2Component, + ChangeLoginPasswordService, + CipherFormConfig, + CipherFormConfigService, + CipherFormGenerationService, + CipherFormMode, + CipherFormModule, + CipherViewComponent, + DecryptionFailureDialogComponent, + DefaultChangeLoginPasswordService, + DefaultCipherFormConfigService, + PasswordRepromptService, +} from "@bitwarden/vault"; + +import { NavComponent } from "../../../app/layout/nav.component"; +import { SearchBarService } from "../../../app/layout/search/search-bar.service"; +import { DesktopCredentialGenerationService } from "../../../services/desktop-cipher-form-generator.service"; +import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service"; +import { invokeMenu, RendererMenuItem } from "../../../utils"; + +import { FolderAddEditComponent } from "./folder-add-edit.component"; +import { ItemFooterComponent } from "./item-footer.component"; +import { VaultFilterComponent } from "./vault-filter/vault-filter.component"; +import { VaultFilterModule } from "./vault-filter/vault-filter.module"; +import { VaultItemsV2Component } from "./vault-items-v2.component"; + +const BroadcasterSubscriptionId = "VaultComponent"; + +@Component({ + selector: "app-vault", + templateUrl: "vault-v2.component.html", + standalone: true, + imports: [ + BadgeModule, + CommonModule, + CipherFormModule, + CipherViewComponent, + ItemFooterComponent, + I18nPipe, + ItemModule, + ButtonModule, + NavComponent, + VaultFilterModule, + VaultItemsV2Component, + ], + providers: [ + { + provide: CipherFormConfigService, + useClass: DefaultCipherFormConfigService, + }, + { + provide: ChangeLoginPasswordService, + useClass: DefaultChangeLoginPasswordService, + }, + { + provide: ViewPasswordHistoryService, + useClass: VaultViewPasswordHistoryService, + }, + { + provide: PremiumUpgradePromptService, + useClass: DesktopPremiumUpgradePromptService, + }, + { provide: CipherFormGenerationService, useClass: DesktopCredentialGenerationService }, + ], +}) +export class VaultV2Component implements OnInit, OnDestroy { + @ViewChild(VaultItemsV2Component, { static: true }) + vaultItemsComponent: VaultItemsV2Component | null = null; + @ViewChild(VaultFilterComponent, { static: true }) + vaultFilterComponent: VaultFilterComponent | null = null; + @ViewChild("folderAddEdit", { read: ViewContainerRef, static: true }) + folderAddEditModalRef: ViewContainerRef | null = null; + + action: CipherFormMode | "view" | null = null; + cipherId: string | null = null; + favorites = false; + type: CipherType | null = null; + folderId: string | null = null; + collectionId: string | null = null; + organizationId: string | null = null; + myVaultOnly = false; + addType: CipherType | undefined = undefined; + addOrganizationId: string | null = null; + addCollectionIds: string[] | null = null; + showingModal = false; + deleted = false; + userHasPremiumAccess = false; + activeFilter: VaultFilter = new VaultFilter(); + activeUserId: UserId | null = null; + cipherRepromptId: string | null = null; + cipher: CipherView | null = new CipherView(); + collections: CollectionView[] | null = null; + config: CipherFormConfig | null = null; + + protected canAccessAttachments$ = this.accountService.activeAccount$.pipe( + filter((account): account is Account => !!account), + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + ); + + private modal: ModalRef | null = null; + private componentIsDestroyed$ = new Subject(); + + constructor( + private route: ActivatedRoute, + private router: Router, + private i18nService: I18nService, + private modalService: ModalService, + private broadcasterService: BroadcasterService, + private changeDetectorRef: ChangeDetectorRef, + private ngZone: NgZone, + private syncService: SyncService, + private messagingService: MessagingService, + private platformUtilsService: PlatformUtilsService, + private eventCollectionService: EventCollectionService, + private totpService: TotpService, + private passwordRepromptService: PasswordRepromptService, + private searchBarService: SearchBarService, + private apiService: ApiService, + private dialogService: DialogService, + private billingAccountProfileStateService: BillingAccountProfileStateService, + private toastService: ToastService, + private accountService: AccountService, + private cipherService: CipherService, + private formConfigService: CipherFormConfigService, + private premiumUpgradePromptService: PremiumUpgradePromptService, + ) {} + + async ngOnInit() { + this.accountService.activeAccount$ + .pipe( + filter((account): account is Account => !!account), + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + takeUntil(this.componentIsDestroyed$), + ) + .subscribe((canAccessPremium: boolean) => { + this.userHasPremiumAccess = canAccessPremium; + }); + + this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { + this.ngZone + .run(async () => { + let detectChanges = true; + try { + switch (message.command) { + case "newLogin": + await this.addCipher(CipherType.Login).catch(() => {}); + break; + case "newCard": + await this.addCipher(CipherType.Card).catch(() => {}); + break; + case "newIdentity": + await this.addCipher(CipherType.Identity).catch(() => {}); + break; + case "newSecureNote": + await this.addCipher(CipherType.SecureNote).catch(() => {}); + break; + case "focusSearch": + (document.querySelector("#search") as HTMLInputElement)?.select(); + detectChanges = false; + break; + case "syncCompleted": + if (this.vaultItemsComponent) { + await this.vaultItemsComponent + .reload(this.activeFilter.buildFilter()) + .catch(() => {}); + } + if (this.vaultFilterComponent) { + await this.vaultFilterComponent + .reloadCollectionsAndFolders(this.activeFilter) + .catch(() => {}); + await this.vaultFilterComponent.reloadOrganizations().catch(() => {}); + } + break; + case "modalShown": + this.showingModal = true; + break; + case "modalClosed": + this.showingModal = false; + break; + case "copyUsername": { + if (this.cipher?.login?.username) { + this.copyValue(this.cipher, this.cipher?.login?.username, "username", "Username"); + } + break; + } + case "copyPassword": { + if (this.cipher?.login?.password && this.cipher.viewPassword) { + this.copyValue(this.cipher, this.cipher.login.password, "password", "Password"); + await this.eventCollectionService + .collect(EventType.Cipher_ClientCopiedPassword, this.cipher.id) + .catch(() => {}); + } + break; + } + case "copyTotp": { + if ( + this.cipher?.login?.hasTotp && + (this.cipher.organizationUseTotp || this.userHasPremiumAccess) + ) { + const value = await firstValueFrom( + this.totpService.getCode$(this.cipher.login.totp), + ).catch(() => null); + if (value) { + this.copyValue(this.cipher, value.code, "verificationCodeTotp", "TOTP"); + } + } + break; + } + default: + detectChanges = false; + break; + } + } catch { + // Ignore errors + } + if (detectChanges) { + this.changeDetectorRef.detectChanges(); + } + }) + .catch(() => {}); + }); + + if (!this.syncService.syncInProgress) { + await this.load().catch(() => {}); + } + + this.searchBarService.setEnabled(true); + this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault")); + + const authRequest = await this.apiService.getLastAuthRequest().catch(() => null); + if (authRequest != null) { + this.messagingService.send("openLoginApproval", { + notificationId: authRequest.id, + }); + } + + this.activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getUserId), + ).catch(() => null); + + if (this.activeUserId) { + this.cipherService + .failedToDecryptCiphers$(this.activeUserId) + .pipe( + map((ciphers) => ciphers?.filter((c) => !c.isDeleted) ?? []), + filter((ciphers) => ciphers.length > 0), + take(1), + takeUntil(this.componentIsDestroyed$), + ) + .subscribe((ciphers) => { + DecryptionFailureDialogComponent.open(this.dialogService, { + cipherIds: ciphers.map((c) => c.id as CipherId), + }); + }); + } + } + + ngOnDestroy() { + this.searchBarService.setEnabled(false); + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + this.componentIsDestroyed$.next(true); + this.componentIsDestroyed$.complete(); + } + + async load() { + const params = await firstValueFrom(this.route.queryParams).catch(); + if (params.cipherId) { + const cipherView = new CipherView(); + cipherView.id = params.cipherId; + if (params.action === "clone") { + await this.cloneCipher(cipherView).catch(() => {}); + } else if (params.action === "edit") { + await this.editCipher(cipherView).catch(() => {}); + } else { + await this.viewCipher(cipherView).catch(() => {}); + } + } else if (params.action === "add") { + this.addType = Number(params.addType); + await this.addCipher(this.addType).catch(() => {}); + } + + this.activeFilter = new VaultFilter({ + status: params.deleted ? "trash" : params.favorites ? "favorites" : "all", + cipherType: + params.action === "add" || params.type == null + ? undefined + : (parseInt(params.type) as CipherType), + selectedFolderId: params.folderId, + selectedCollectionId: params.selectedCollectionId, + selectedOrganizationId: params.selectedOrganizationId, + myVaultOnly: params.myVaultOnly ?? false, + }); + if (this.vaultItemsComponent) { + await this.vaultItemsComponent.reload(this.activeFilter.buildFilter()).catch(() => {}); + } + } + + async viewCipher(cipher: CipherView) { + if (await this.shouldReprompt(cipher, "view")) { + return; + } + this.cipherId = cipher.id; + this.cipher = cipher; + this.collections = + this.vaultFilterComponent?.collections.fullList.filter((c) => + cipher.collectionIds.includes(c.id), + ) ?? null; + this.action = "view"; + await this.go().catch(() => {}); + } + + async openAttachmentsDialog() { + if (!this.userHasPremiumAccess) { + await this.premiumUpgradePromptService.promptForPremium(); + return; + } + const dialogRef = AttachmentsV2Component.open(this.dialogService, { + cipherId: this.cipherId as CipherId, + }); + const result = await firstValueFrom(dialogRef.closed).catch(() => null); + if ( + result?.action === AttachmentDialogResult.Removed || + result?.action === AttachmentDialogResult.Uploaded + ) { + await this.vaultItemsComponent?.refresh().catch(() => {}); + } + } + + viewCipherMenu(cipher: CipherView) { + const menu: RendererMenuItem[] = [ + { + label: this.i18nService.t("view"), + click: () => { + this.functionWithChangeDetection(() => { + this.viewCipher(cipher).catch(() => {}); + }); + }, + }, + ]; + + if (cipher.decryptionFailure) { + invokeMenu(menu); + return; + } + + if (!cipher.isDeleted) { + menu.push({ + label: this.i18nService.t("edit"), + click: () => { + this.functionWithChangeDetection(() => { + this.editCipher(cipher).catch(() => {}); + }); + }, + }); + if (!cipher.organizationId) { + menu.push({ + label: this.i18nService.t("clone"), + click: () => { + this.functionWithChangeDetection(() => { + this.cloneCipher(cipher).catch(() => {}); + }); + }, + }); + } + } + + switch (cipher.type) { + case CipherType.Login: + if ( + cipher.login.canLaunch || + cipher.login.username != null || + cipher.login.password != null + ) { + menu.push({ type: "separator" }); + } + if (cipher.login.canLaunch) { + menu.push({ + label: this.i18nService.t("launch"), + click: () => this.platformUtilsService.launchUri(cipher.login.launchUri), + }); + } + if (cipher.login.username != null) { + menu.push({ + label: this.i18nService.t("copyUsername"), + click: () => this.copyValue(cipher, cipher.login.username, "username", "Username"), + }); + } + if (cipher.login.password != null && cipher.viewPassword) { + menu.push({ + label: this.i18nService.t("copyPassword"), + click: () => { + this.copyValue(cipher, cipher.login.password, "password", "Password"); + this.eventCollectionService + .collect(EventType.Cipher_ClientCopiedPassword, cipher.id) + .catch(() => {}); + }, + }); + } + if (cipher.login.hasTotp && (cipher.organizationUseTotp || this.userHasPremiumAccess)) { + menu.push({ + label: this.i18nService.t("copyVerificationCodeTotp"), + click: async () => { + const value = await firstValueFrom( + this.totpService.getCode$(cipher.login.totp), + ).catch(() => null); + if (value) { + this.copyValue(cipher, value.code, "verificationCodeTotp", "TOTP"); + } + }, + }); + } + break; + case CipherType.Card: + if (cipher.card.number != null || cipher.card.code != null) { + menu.push({ type: "separator" }); + } + if (cipher.card.number != null) { + menu.push({ + label: this.i18nService.t("copyNumber"), + click: () => this.copyValue(cipher, cipher.card.number, "number", "Card Number"), + }); + } + if (cipher.card.code != null) { + menu.push({ + label: this.i18nService.t("copySecurityCode"), + click: () => { + this.copyValue(cipher, cipher.card.code, "securityCode", "Security Code"); + this.eventCollectionService + .collect(EventType.Cipher_ClientCopiedCardCode, cipher.id) + .catch(() => {}); + }, + }); + } + break; + default: + break; + } + invokeMenu(menu); + } + + async shouldReprompt(cipher: CipherView, action: "edit" | "clone" | "view"): Promise { + return !(await this.canNavigateAway(action, cipher)) || !(await this.passwordReprompt(cipher)); + } + + async buildFormConfig(action: CipherFormMode) { + this.config = await this.formConfigService + .buildConfig(action, this.cipherId as CipherId, this.addType) + .catch(() => null); + } + + async editCipher(cipher: CipherView) { + if (await this.shouldReprompt(cipher, "edit")) { + return; + } + this.cipherId = cipher.id; + this.cipher = cipher; + await this.buildFormConfig("edit"); + this.action = "edit"; + await this.go().catch(() => {}); + } + + async cloneCipher(cipher: CipherView) { + if (await this.shouldReprompt(cipher, "clone")) { + return; + } + this.cipherId = cipher.id; + this.cipher = cipher; + await this.buildFormConfig("clone"); + this.action = "clone"; + await this.go().catch(() => {}); + } + + async addCipher(type: CipherType) { + this.addType = type || this.activeFilter.cipherType; + this.cipherId = null; + await this.buildFormConfig("add"); + this.action = "add"; + this.prefillCipherFromFilter(); + await this.go().catch(() => {}); + } + + addCipherOptions() { + const menu: RendererMenuItem[] = [ + { + label: this.i18nService.t("typeLogin"), + click: () => this.addCipherWithChangeDetection(CipherType.Login), + }, + { + label: this.i18nService.t("typeCard"), + click: () => this.addCipherWithChangeDetection(CipherType.Card), + }, + { + label: this.i18nService.t("typeIdentity"), + click: () => this.addCipherWithChangeDetection(CipherType.Identity), + }, + { + label: this.i18nService.t("typeSecureNote"), + click: () => this.addCipherWithChangeDetection(CipherType.SecureNote), + }, + ]; + invokeMenu(menu); + } + + async savedCipher(cipher: CipherView) { + this.cipherId = null; + this.action = "view"; + await this.vaultItemsComponent?.refresh().catch(() => {}); + this.cipherId = cipher.id; + this.cipher = cipher; + if (this.activeUserId) { + await this.cipherService.clearCache(this.activeUserId).catch(() => {}); + } + await this.vaultItemsComponent?.load(this.activeFilter.buildFilter()).catch(() => {}); + await this.go().catch(() => {}); + await this.vaultItemsComponent?.refresh().catch(() => {}); + } + + async deleteCipher() { + this.cipherId = null; + this.cipher = null; + this.action = null; + await this.go().catch(() => {}); + await this.vaultItemsComponent?.refresh().catch(() => {}); + } + + async restoreCipher() { + this.cipherId = null; + this.action = null; + await this.go().catch(() => {}); + await this.vaultItemsComponent?.refresh().catch(() => {}); + } + + async cancelCipher(cipher: CipherView) { + this.cipherId = cipher.id; + this.cipher = cipher; + this.action = this.cipherId != null ? "view" : null; + await this.go().catch(() => {}); + } + + async applyVaultFilter(vaultFilter: VaultFilter) { + this.searchBarService.setPlaceholderText( + this.i18nService.t(this.calculateSearchBarLocalizationString(vaultFilter)), + ); + this.activeFilter = vaultFilter; + await this.vaultItemsComponent + ?.reload(this.activeFilter.buildFilter(), vaultFilter.status === "trash") + .catch(() => {}); + await this.go().catch(() => {}); + } + + private calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string { + if (vaultFilter.status === "favorites") { + return "searchFavorites"; + } + if (vaultFilter.status === "trash") { + return "searchTrash"; + } + if (vaultFilter.cipherType != null) { + return "searchType"; + } + if (vaultFilter.selectedFolderId != null && vaultFilter.selectedFolderId !== "none") { + return "searchFolder"; + } + if (vaultFilter.selectedCollectionId != null) { + return "searchCollection"; + } + if (vaultFilter.selectedOrganizationId != null) { + return "searchOrganization"; + } + if (vaultFilter.myVaultOnly) { + return "searchMyVault"; + } + return "searchVault"; + } + + async addFolder() { + this.messagingService.send("newFolder"); + } + + async editFolder(folderId: string) { + if (this.modal != null) { + this.modal.close(); + } + if (this.folderAddEditModalRef == null) { + return; + } + const [modal, childComponent] = await this.modalService + .openViewRef( + FolderAddEditComponent, + this.folderAddEditModalRef, + (comp) => (comp.folderId = folderId), + ) + .catch(() => [null, null] as any); + this.modal = modal; + if (childComponent) { + childComponent.onSavedFolder.subscribe(async (folder: FolderView) => { + this.modal?.close(); + await this.vaultFilterComponent + ?.reloadCollectionsAndFolders(this.activeFilter) + .catch(() => {}); + }); + childComponent.onDeletedFolder.subscribe(async (folder: FolderView) => { + this.modal?.close(); + await this.vaultFilterComponent + ?.reloadCollectionsAndFolders(this.activeFilter) + .catch(() => {}); + }); + } + if (this.modal) { + this.modal.onClosed.pipe(takeUntilDestroyed()).subscribe(() => { + this.modal = null; + }); + } + } + + private dirtyInput(): boolean { + return ( + (this.action === "add" || this.action === "edit" || this.action === "clone") && + document.querySelectorAll("vault-cipher-form .ng-dirty").length > 0 + ); + } + + private async wantsToSaveChanges(): Promise { + const confirmed = await this.dialogService + .openSimpleDialog({ + title: { key: "unsavedChangesTitle" }, + content: { key: "unsavedChangesConfirmation" }, + type: "warning", + }) + .catch(() => false); + return !confirmed; + } + + private async go(queryParams: any = null) { + if (queryParams == null) { + queryParams = { + action: this.action, + cipherId: this.cipherId, + favorites: this.favorites ? true : null, + type: this.type, + folderId: this.folderId, + collectionId: this.collectionId, + deleted: this.deleted ? true : null, + organizationId: this.organizationId, + myVaultOnly: this.myVaultOnly, + }; + } + this.router + .navigate([], { + relativeTo: this.route, + queryParams: queryParams, + replaceUrl: true, + }) + .catch(() => {}); + } + + private addCipherWithChangeDetection(type: CipherType) { + this.functionWithChangeDetection(() => this.addCipher(type).catch(() => {})); + } + + private copyValue(cipher: CipherView, value: string, labelI18nKey: string, aType: string) { + this.functionWithChangeDetection(() => { + (async () => { + if ( + cipher.reprompt !== CipherRepromptType.None && + this.passwordRepromptService.protectedFields().includes(aType) && + !(await this.passwordReprompt(cipher)) + ) { + return; + } + this.platformUtilsService.copyToClipboard(value); + this.toastService.showToast({ + variant: "info", + title: undefined, + message: this.i18nService.t("valueCopied", this.i18nService.t(labelI18nKey)), + }); + if (this.action === "view") { + this.messagingService.send("minimizeOnCopy"); + } + })().catch(() => {}); + }); + } + + private functionWithChangeDetection(func: () => void) { + this.ngZone.run(() => { + func(); + this.changeDetectorRef.detectChanges(); + }); + } + + private prefillCipherFromFilter() { + if (this.activeFilter.selectedCollectionId != null && this.vaultFilterComponent != null) { + const collections = this.vaultFilterComponent.collections.fullList.filter( + (c) => c.id === this.activeFilter.selectedCollectionId, + ); + if (collections.length > 0) { + this.addOrganizationId = collections[0].organizationId; + this.addCollectionIds = [this.activeFilter.selectedCollectionId]; + } + } else if (this.activeFilter.selectedOrganizationId) { + this.addOrganizationId = this.activeFilter.selectedOrganizationId; + } + if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) { + this.folderId = this.activeFilter.selectedFolderId; + } + } + + private async canNavigateAway(action: string, cipher?: CipherView) { + if (this.action === action && (!cipher || this.cipherId === cipher.id)) { + return false; + } else if (this.dirtyInput() && (await this.wantsToSaveChanges())) { + return false; + } + return true; + } + + private async passwordReprompt(cipher: CipherView) { + if (cipher.reprompt === CipherRepromptType.None) { + this.cipherRepromptId = null; + return true; + } + if (this.cipherRepromptId === cipher.id) { + return true; + } + const repromptResult = await this.passwordRepromptService.showPasswordPrompt(); + if (repromptResult) { + this.cipherRepromptId = cipher.id; + } + return repromptResult; + } +} diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index 97193bf1b1f..8cb54d9a911 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -69,6 +69,8 @@ import { ToastService, } from "@bitwarden/components"; import { + AttachmentDialogResult, + AttachmentsV2Component, CipherFormConfig, CipherFormConfigService, CollectionAssignmentResult, @@ -92,10 +94,6 @@ import { } from "../../../vault/components/vault-item-dialog/vault-item-dialog.component"; import { VaultItemEvent } from "../../../vault/components/vault-items/vault-item-event"; import { VaultItemsModule } from "../../../vault/components/vault-items/vault-items.module"; -import { - AttachmentDialogResult, - AttachmentsV2Component, -} from "../../../vault/individual-vault/attachments-v2.component"; import { BulkDeleteDialogResult, openBulkDeleteDialog, diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts index bcae11c3264..0022da7f3a9 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts @@ -2,6 +2,7 @@ import { CommonModule } from "@angular/common"; import { Component, Inject } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { EmergencyAccessId } from "@bitwarden/common/types/guid"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; @@ -21,8 +22,6 @@ import { DefaultChangeLoginPasswordService, } from "@bitwarden/vault"; -import { WebViewPasswordHistoryService } from "../../../../vault/services/web-view-password-history.service"; - export interface EmergencyViewDialogParams { /** The cipher being viewed. */ cipher: CipherView; @@ -42,7 +41,7 @@ class PremiumUpgradePromptNoop implements PremiumUpgradePromptService { standalone: true, imports: [ButtonModule, CipherViewComponent, DialogModule, CommonModule, JslibModule], providers: [ - { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, + { provide: ViewPasswordHistoryService, useClass: VaultViewPasswordHistoryService }, { provide: PremiumUpgradePromptService, useClass: PremiumUpgradePromptNoop }, { provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService }, ], diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 10466503029..99159e7e2fc 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -7,6 +7,7 @@ import { firstValueFrom, Subject, switchMap } from "rxjs"; import { map } from "rxjs/operators"; import { CollectionView } from "@bitwarden/admin-console/common"; +import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -39,6 +40,9 @@ import { ToastService, } from "@bitwarden/components"; import { + AttachmentDialogCloseResult, + AttachmentDialogResult, + AttachmentsV2Component, ChangeLoginPasswordService, CipherFormComponent, CipherFormConfig, @@ -50,16 +54,10 @@ import { } from "@bitwarden/vault"; import { SharedModule } from "../../../shared/shared.module"; -import { - AttachmentDialogCloseResult, - AttachmentDialogResult, - AttachmentsV2Component, -} from "../../individual-vault/attachments-v2.component"; +import { WebVaultPremiumUpgradePromptService } from "../../../vault/services/web-premium-upgrade-prompt.service"; import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; import { RoutedVaultFilterModel } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { WebCipherFormGenerationService } from "../../services/web-cipher-form-generation.service"; -import { WebVaultPremiumUpgradePromptService } from "../../services/web-premium-upgrade-prompt.service"; -import { WebViewPasswordHistoryService } from "../../services/web-view-password-history.service"; export type VaultItemDialogMode = "view" | "form"; @@ -135,7 +133,7 @@ export enum VaultItemDialogResult { ], providers: [ { provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService }, - { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, + { provide: ViewPasswordHistoryService, useClass: VaultViewPasswordHistoryService }, { provide: CipherFormGenerationService, useClass: WebCipherFormGenerationService }, RoutedVaultFilterService, { provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService }, diff --git a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts index d9b42594d79..2eab6faec36 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts @@ -21,6 +21,7 @@ import { ItemModule, } from "@bitwarden/components"; import { + AttachmentsV2Component, CipherAttachmentsComponent, CipherFormConfig, CipherFormGenerationService, @@ -31,8 +32,6 @@ import { import { SharedModule } from "../../shared/shared.module"; import { WebCipherFormGenerationService } from "../services/web-cipher-form-generation.service"; -import { AttachmentsV2Component } from "./attachments-v2.component"; - /** * The result of the AddEditCipherDialogV2 component. */ diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 7055f164a53..5f56ecc9e04 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -69,6 +69,9 @@ import { DialogRef, DialogService, Icons, ToastService } from "@bitwarden/compon import { AddEditFolderDialogComponent, AddEditFolderDialogResult, + AttachmentDialogCloseResult, + AttachmentDialogResult, + AttachmentsV2Component, CipherFormConfig, CollectionAssignmentResult, DecryptionFailureDialogComponent, @@ -96,11 +99,6 @@ import { VaultItem } from "../components/vault-items/vault-item"; import { VaultItemEvent } from "../components/vault-items/vault-item-event"; import { VaultItemsModule } from "../components/vault-items/vault-items.module"; -import { - AttachmentDialogCloseResult, - AttachmentDialogResult, - AttachmentsV2Component, -} from "./attachments-v2.component"; import { BulkDeleteDialogResult, openBulkDeleteDialog, diff --git a/apps/web/src/app/vault/individual-vault/view.component.ts b/apps/web/src/app/vault/individual-vault/view.component.ts index 3e5cced7fa8..e7b06cbb8d6 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.ts +++ b/apps/web/src/app/vault/individual-vault/view.component.ts @@ -5,6 +5,7 @@ import { Component, EventEmitter, Inject, OnInit } from "@angular/core"; import { firstValueFrom, map, Observable } from "rxjs"; import { CollectionView } from "@bitwarden/admin-console/common"; +import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -21,8 +22,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { DIALOG_DATA, - DialogConfig, DialogRef, + DialogConfig, AsyncActionsModule, DialogModule, DialogService, @@ -31,8 +32,7 @@ import { import { CipherViewComponent } from "@bitwarden/vault"; import { SharedModule } from "../../shared/shared.module"; -import { WebVaultPremiumUpgradePromptService } from "../services/web-premium-upgrade-prompt.service"; -import { WebViewPasswordHistoryService } from "../services/web-view-password-history.service"; +import { WebVaultPremiumUpgradePromptService } from "../../vault/services/web-premium-upgrade-prompt.service"; export interface ViewCipherDialogParams { cipher: CipherView; @@ -74,7 +74,7 @@ export interface ViewCipherDialogCloseResult { standalone: true, imports: [CipherViewComponent, CommonModule, AsyncActionsModule, DialogModule, SharedModule], providers: [ - { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, + { provide: ViewPasswordHistoryService, useClass: VaultViewPasswordHistoryService }, { provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService }, ], }) diff --git a/apps/web/src/app/vault/services/web-view-password-history.service.spec.ts b/libs/angular/src/services/view-password-history.service.spec.ts similarity index 69% rename from apps/web/src/app/vault/services/web-view-password-history.service.spec.ts rename to libs/angular/src/services/view-password-history.service.spec.ts index a4f73ed1a2e..dec2b25b190 100644 --- a/apps/web/src/app/vault/services/web-view-password-history.service.spec.ts +++ b/libs/angular/src/services/view-password-history.service.spec.ts @@ -3,17 +3,16 @@ import { TestBed } from "@angular/core/testing"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; +import { openPasswordHistoryDialog } from "@bitwarden/vault"; -import { openPasswordHistoryDialog } from "../individual-vault/password-history.component"; +import { VaultViewPasswordHistoryService } from "./view-password-history.service"; -import { WebViewPasswordHistoryService } from "./web-view-password-history.service"; - -jest.mock("../individual-vault/password-history.component", () => ({ +jest.mock("@bitwarden/vault", () => ({ openPasswordHistoryDialog: jest.fn(), })); -describe("WebViewPasswordHistoryService", () => { - let service: WebViewPasswordHistoryService; +describe("VaultViewPasswordHistoryService", () => { + let service: VaultViewPasswordHistoryService; let dialogService: DialogService; beforeEach(async () => { @@ -23,13 +22,13 @@ describe("WebViewPasswordHistoryService", () => { await TestBed.configureTestingModule({ providers: [ - WebViewPasswordHistoryService, + VaultViewPasswordHistoryService, { provide: DialogService, useValue: mockDialogService }, Overlay, ], }).compileComponents(); - service = TestBed.inject(WebViewPasswordHistoryService); + service = TestBed.inject(VaultViewPasswordHistoryService); dialogService = TestBed.inject(DialogService); }); diff --git a/apps/web/src/app/vault/services/web-view-password-history.service.ts b/libs/angular/src/services/view-password-history.service.ts similarity index 78% rename from apps/web/src/app/vault/services/web-view-password-history.service.ts rename to libs/angular/src/services/view-password-history.service.ts index b1451b268de..88ca4d37287 100644 --- a/apps/web/src/app/vault/services/web-view-password-history.service.ts +++ b/libs/angular/src/services/view-password-history.service.ts @@ -3,14 +3,13 @@ import { Injectable } from "@angular/core"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; - -import { openPasswordHistoryDialog } from "../individual-vault/password-history.component"; +import { openPasswordHistoryDialog } from "@bitwarden/vault"; /** - * This service is used to display the password history dialog in the web vault. + * This service is used to display the password history dialog in the vault. */ @Injectable() -export class WebViewPasswordHistoryService implements ViewPasswordHistoryService { +export class VaultViewPasswordHistoryService implements ViewPasswordHistoryService { constructor(private dialogService: DialogService) {} /** diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index 852302cc0c4..c34816994be 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -18,6 +18,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @Directive() @@ -25,13 +26,14 @@ export class VaultItemsComponent implements OnInit, OnDestroy { @Input() activeCipherId: string = null; @Output() onCipherClicked = new EventEmitter(); @Output() onCipherRightClicked = new EventEmitter(); - @Output() onAddCipher = new EventEmitter(); + @Output() onAddCipher = new EventEmitter(); @Output() onAddCipherOptions = new EventEmitter(); loaded = false; ciphers: CipherView[] = []; deleted = false; organization: Organization; + CipherType = CipherType; protected searchPending = false; @@ -109,8 +111,8 @@ export class VaultItemsComponent implements OnInit, OnDestroy { this.onCipherRightClicked.emit(cipher); } - addCipher() { - this.onAddCipher.emit(); + addCipher(type?: CipherType) { + this.onAddCipher.emit(type); } addCipherOptions() { diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 33843932382..e353d79988f 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -57,6 +57,7 @@ export enum FeatureFlag { VaultBulkManagementAction = "vault-bulk-management-action", SecurityTasks = "security-tasks", CipherKeyEncryption = "cipher-key-encryption", + PM18520_UpdateDesktopCipherForm = "pm-18520-desktop-cipher-forms", EndUserNotifications = "pm-10609-end-user-notifications", /* Platform */ @@ -110,6 +111,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.VaultBulkManagementAction]: FALSE, [FeatureFlag.SecurityTasks]: FALSE, [FeatureFlag.CipherKeyEncryption]: FALSE, + [FeatureFlag.PM18520_UpdateDesktopCipherForm]: FALSE, [FeatureFlag.EndUserNotifications]: FALSE, /* Auth */ diff --git a/libs/vault/src/cipher-form/components/identity/identity.component.ts b/libs/vault/src/cipher-form/components/identity/identity.component.ts index 02bee40c2c7..f0c73002e97 100644 --- a/libs/vault/src/cipher-form/components/identity/identity.component.ts +++ b/libs/vault/src/cipher-form/components/identity/identity.component.ts @@ -132,22 +132,22 @@ export class IdentitySectionComponent implements OnInit { private initFromExistingCipher(existingIdentity: IdentityView) { this.identityForm.patchValue({ - firstName: this.initialValues.firstName ?? existingIdentity.firstName, - middleName: this.initialValues.middleName ?? existingIdentity.middleName, - lastName: this.initialValues.lastName ?? existingIdentity.lastName, - company: this.initialValues.company ?? existingIdentity.company, - ssn: this.initialValues.ssn ?? existingIdentity.ssn, - passportNumber: this.initialValues.passportNumber ?? existingIdentity.passportNumber, - licenseNumber: this.initialValues.licenseNumber ?? existingIdentity.licenseNumber, - email: this.initialValues.email ?? existingIdentity.email, - phone: this.initialValues.phone ?? existingIdentity.phone, - address1: this.initialValues.address1 ?? existingIdentity.address1, - address2: this.initialValues.address2 ?? existingIdentity.address2, - address3: this.initialValues.address3 ?? existingIdentity.address3, - city: this.initialValues.city ?? existingIdentity.city, - state: this.initialValues.state ?? existingIdentity.state, - postalCode: this.initialValues.postalCode ?? existingIdentity.postalCode, - country: this.initialValues.country ?? existingIdentity.country, + firstName: this.initialValues?.firstName ?? existingIdentity.firstName, + middleName: this.initialValues?.middleName ?? existingIdentity.middleName, + lastName: this.initialValues?.lastName ?? existingIdentity.lastName, + company: this.initialValues?.company ?? existingIdentity.company, + ssn: this.initialValues?.ssn ?? existingIdentity.ssn, + passportNumber: this.initialValues?.passportNumber ?? existingIdentity.passportNumber, + licenseNumber: this.initialValues?.licenseNumber ?? existingIdentity.licenseNumber, + email: this.initialValues?.email ?? existingIdentity.email, + phone: this.initialValues?.phone ?? existingIdentity.phone, + address1: this.initialValues?.address1 ?? existingIdentity.address1, + address2: this.initialValues?.address2 ?? existingIdentity.address2, + address3: this.initialValues?.address3 ?? existingIdentity.address3, + city: this.initialValues?.city ?? existingIdentity.city, + state: this.initialValues?.state ?? existingIdentity.state, + postalCode: this.initialValues?.postalCode ?? existingIdentity.postalCode, + country: this.initialValues?.country ?? existingIdentity.country, }); } diff --git a/apps/web/src/app/vault/individual-vault/attachments-v2.component.html b/libs/vault/src/cipher-view/attachments/attachments-v2.component.html similarity index 100% rename from apps/web/src/app/vault/individual-vault/attachments-v2.component.html rename to libs/vault/src/cipher-view/attachments/attachments-v2.component.html diff --git a/apps/web/src/app/vault/individual-vault/attachments-v2.component.spec.ts b/libs/vault/src/cipher-view/attachments/attachments-v2.component.spec.ts similarity index 100% rename from apps/web/src/app/vault/individual-vault/attachments-v2.component.spec.ts rename to libs/vault/src/cipher-view/attachments/attachments-v2.component.spec.ts diff --git a/apps/web/src/app/vault/individual-vault/attachments-v2.component.ts b/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts similarity index 84% rename from apps/web/src/app/vault/individual-vault/attachments-v2.component.ts rename to libs/vault/src/cipher-view/attachments/attachments-v2.component.ts index 949a2fa81d9..fd823353099 100644 --- a/apps/web/src/app/vault/individual-vault/attachments-v2.component.ts +++ b/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts @@ -4,10 +4,16 @@ import { CommonModule } from "@angular/common"; import { Component, Inject } from "@angular/core"; import { CipherId } from "@bitwarden/common/types/guid"; -import { DialogRef, DIALOG_DATA, DialogService } from "@bitwarden/components"; -import { CipherAttachmentsComponent } from "@bitwarden/vault"; +import { + ButtonModule, + DialogModule, + DialogService, + DIALOG_DATA, + DialogRef, +} from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; -import { SharedModule } from "../../shared/shared.module"; +import { CipherAttachmentsComponent } from "../../cipher-form/components/attachments/cipher-attachments.component"; export interface AttachmentsDialogParams { cipherId: CipherId; @@ -33,7 +39,7 @@ export interface AttachmentDialogCloseResult { selector: "app-vault-attachments-v2", templateUrl: "attachments-v2.component.html", standalone: true, - imports: [CommonModule, SharedModule, CipherAttachmentsComponent], + imports: [ButtonModule, CommonModule, DialogModule, I18nPipe, CipherAttachmentsComponent], }) export class AttachmentsV2Component { cipherId: CipherId; diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index 48b70271e43..e748fa46656 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -124,7 +124,8 @@ export class CipherViewComponent implements OnChanges, OnDestroy { } const { username, password, totp, fido2Credentials } = this.cipher.login; - return username || password || totp || fido2Credentials; + + return username || password || totp || fido2Credentials?.length > 0; } get hasAutofill() { diff --git a/libs/vault/src/cipher-view/index.ts b/libs/vault/src/cipher-view/index.ts index ec3c5235077..9bcdaaa4385 100644 --- a/libs/vault/src/cipher-view/index.ts +++ b/libs/vault/src/cipher-view/index.ts @@ -1,2 +1,3 @@ export * from "./cipher-view.component"; +export * from "./attachments/attachments-v2.component"; export { CipherAttachmentsComponent } from "../cipher-form/components/attachments/cipher-attachments.component"; diff --git a/apps/web/src/app/vault/individual-vault/password-history.component.html b/libs/vault/src/components/password-history/password-history.component.html similarity index 100% rename from apps/web/src/app/vault/individual-vault/password-history.component.html rename to libs/vault/src/components/password-history/password-history.component.html diff --git a/apps/web/src/app/vault/individual-vault/password-history.component.ts b/libs/vault/src/components/password-history/password-history.component.ts similarity index 95% rename from apps/web/src/app/vault/individual-vault/password-history.component.ts rename to libs/vault/src/components/password-history/password-history.component.ts index 5207e1b9e40..5af785d1a70 100644 --- a/apps/web/src/app/vault/individual-vault/password-history.component.ts +++ b/libs/vault/src/components/password-history/password-history.component.ts @@ -5,17 +5,17 @@ import { Inject, Component } from "@angular/core"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { - DIALOG_DATA, - DialogConfig, - DialogRef, AsyncActionsModule, + ButtonModule, DialogModule, DialogService, + DIALOG_DATA, + DialogRef, + DialogConfig, } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; import { PasswordHistoryViewComponent } from "@bitwarden/vault"; -import { SharedModule } from "../../shared/shared.module"; - /** * The parameters for the password history dialog. */ @@ -31,10 +31,11 @@ export interface ViewPasswordHistoryDialogParams { templateUrl: "password-history.component.html", standalone: true, imports: [ + ButtonModule, CommonModule, AsyncActionsModule, + I18nPipe, DialogModule, - SharedModule, PasswordHistoryViewComponent, ], }) diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index 655b536de64..6e5a452ec8c 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -19,6 +19,7 @@ export { PasswordHistoryViewComponent } from "./components/password-history-view export { NewDeviceVerificationNoticePageOneComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-one.component"; export { NewDeviceVerificationNoticePageTwoComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-two.component"; export { DecryptionFailureDialogComponent } from "./components/decryption-failure-dialog/decryption-failure-dialog.component"; +export { openPasswordHistoryDialog } from "./components/password-history/password-history.component"; export * from "./components/add-edit-folder-dialog/add-edit-folder-dialog.component"; export * from "./components/carousel"; From 320d4f65fa6ce0fd3bb9cdc526e6c39ddb66f13a Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Wed, 23 Apr 2025 14:47:09 -0400 Subject: [PATCH 7/8] PM-20396 open view item vault pop out (#14342) * PM-20396 open view item vault pop out * add aria and clean up * format json * clean naming in messages * revert feature-flag.enum.ts * change username to item name * return nullish operator removed in testing * update tests to account for itemName * revert to anchor tag --- apps/browser/src/_locales/en/messages.json | 43 +++++++++++-------- .../abstractions/notification.background.ts | 4 ++ .../notification.background.spec.ts | 11 +++-- .../background/notification.background.ts | 28 ++++++++++-- .../notification/confirmation/body.ts | 5 ++- .../notification/confirmation/container.ts | 19 ++++---- .../notification/confirmation/message.ts | 17 +++++++- .../abstractions/notification-bar.ts | 2 +- apps/browser/src/autofill/notification/bar.ts | 16 +++---- 9 files changed, 99 insertions(+), 46 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 462af12a352..d1d05a4e852 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -192,7 +192,7 @@ "message": "Copy", "description": "Copy to clipboard" }, - "fill":{ + "fill": { "message": "Fill", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, @@ -1062,6 +1062,15 @@ "notificationAddSave": { "message": "Save" }, + "notificationViewAria": { + "message": "View $ITEMNAME$, opens in new window", + "placeholders": { + "itemName": { + "content": "$1" + } + }, + "description": "Aria label for the view button in notification bar confirmation message" + }, "newNotification": { "message": "New notification" }, @@ -1075,23 +1084,23 @@ } } }, - "loginSaveSuccessDetails": { - "message": "$USERNAME$ saved to Bitwarden.", - "placeholders": { - "username": { - "content": "$1" - } - }, - "description": "Shown to user after login is saved." + "loginSaveConfirmation": { + "message": "$ITEMNAME$ saved to Bitwarden.", + "placeholders": { + "itemName": { + "content": "$1" + } + }, + "description": "Shown to user after item is saved." }, - "loginUpdatedSuccessDetails": { - "message": "$USERNAME$ updated in Bitwarden.", - "placeholders": { - "username": { - "content": "$1" - } - }, - "description": "Shown to user after login is updated." + "loginUpdatedConfirmation": { + "message": "$ITEMNAME$ updated in Bitwarden.", + "placeholders": { + "itemName": { + "content": "$1" + } + }, + "description": "Shown to user after item is updated." }, "saveAsNewLoginAction": { "message": "Save as new login", diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index 6b3c91a109c..c93fd9a3acf 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -101,6 +101,10 @@ type NotificationBackgroundExtensionMessageHandlers = { bgChangedPassword: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgRemoveTabFromNotificationQueue: ({ sender }: BackgroundSenderParam) => void; bgSaveCipher: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + bgOpenViewVaultItemPopout: ({ + message, + sender, + }: BackgroundOnMessageHandlerParams) => Promise; bgOpenVault: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgNeverSave: ({ sender }: BackgroundSenderParam) => Promise; bgUnlockPopoutOpened: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index ffc416ab62a..bb993fcf94b 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -823,6 +823,7 @@ describe("NotificationBackground", () => { notificationBackground["notificationQueue"] = [queueMessage]; const cipherView = mock({ id: "testId", + name: "testItemName", login: { username: "testUser" }, }); getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView); @@ -844,8 +845,9 @@ describe("NotificationBackground", () => { sender.tab, "saveCipherAttemptCompleted", { - username: cipherView.login.username, + itemName: "testItemName", cipherId: cipherView.id, + task: undefined, }, ); }); @@ -899,7 +901,7 @@ describe("NotificationBackground", () => { const cipherView = mock({ id: mockCipherId, organizationId: mockOrgId, - login: { username: "testUser" }, + name: "Test Item", }); getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView); @@ -921,11 +923,11 @@ describe("NotificationBackground", () => { "saveCipherAttemptCompleted", { cipherId: "testId", + itemName: "Test Item", task: { orgName: "Org Name, LLC", remainingTasksCount: 1, }, - username: "testUser", }, ); }); @@ -1074,6 +1076,7 @@ describe("NotificationBackground", () => { notificationBackground["notificationQueue"] = [queueMessage]; const cipherView = mock({ id: "testId", + name: "testName", login: { username: "test", password: "password" }, }); folderExistsSpy.mockResolvedValueOnce(false); @@ -1097,8 +1100,8 @@ describe("NotificationBackground", () => { sender.tab, "saveCipherAttemptCompleted", { - username: cipherView.login.username, cipherId: cipherView.id, + itemName: cipherView.name, }, ); expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { command: "addedCipher" }); diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 00d24300d78..dabb75b97b6 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -41,7 +41,10 @@ import { SecurityTask } from "@bitwarden/common/vault/tasks/models/security-task import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; import { BrowserApi } from "../../platform/browser/browser-api"; -import { openAddEditVaultItemPopout } from "../../vault/popup/utils/vault-popout-window"; +import { + openAddEditVaultItemPopout, + openViewVaultItemPopout, +} from "../../vault/popup/utils/vault-popout-window"; import { OrganizationCategory, OrganizationCategories, @@ -67,6 +70,7 @@ import { OverlayBackgroundExtensionMessage } from "./abstractions/overlay.backgr export default class NotificationBackground { private openUnlockPopout = openUnlockPopout; private openAddEditVaultItemPopout = openAddEditVaultItemPopout; + private openViewVaultItemPopout = openViewVaultItemPopout; private notificationQueue: NotificationQueueMessageItem[] = []; private allowedRetryCommands: Set = new Set([ ExtensionCommand.AutofillLogin, @@ -91,6 +95,7 @@ export default class NotificationBackground { bgGetOrgData: () => this.getOrgData(), bgNeverSave: ({ sender }) => this.saveNever(sender.tab), bgOpenVault: ({ message, sender }) => this.openVault(message, sender.tab), + bgOpenViewVaultItemPopout: ({ message, sender }) => this.viewItem(message, sender.tab), bgRemoveTabFromNotificationQueue: ({ sender }) => this.removeTabFromNotificationQueue(sender.tab), bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab), @@ -638,8 +643,8 @@ export default class NotificationBackground { try { await this.cipherService.createWithServer(cipher); await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { - username: queueMessage?.username && String(queueMessage.username), - cipherId: cipher?.id && String(cipher.id), + itemName: newCipher?.name && String(newCipher?.name), + cipherId: cipher?.id && String(cipher?.id), }); await BrowserApi.tabSendMessage(tab, { command: "addedCipher" }); } catch (error) { @@ -701,7 +706,7 @@ export default class NotificationBackground { await this.cipherService.updateWithServer(cipher); await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { - username: cipherView?.login?.username && String(cipherView.login.username), + itemName: cipherView?.name && String(cipherView?.name), cipherId: cipherView?.id && String(cipherView.id), task: taskData, }); @@ -754,6 +759,21 @@ export default class NotificationBackground { await this.openAddEditVaultItemPopout(senderTab, { cipherId: message.cipherId }); } + private async viewItem( + message: NotificationBackgroundExtensionMessage, + senderTab: chrome.tabs.Tab, + ) { + await Promise.all([ + this.openViewVaultItemPopout(senderTab, { + cipherId: message.cipherId, + action: null, + }), + BrowserApi.tabSendMessageData(senderTab, "closeNotificationBar", { + fadeOutNotification: !!message.fadeOutNotification, + }), + ]); + } + private async folderExists(folderId: string, userId: UserId) { if (Utils.isNullOrWhitespace(folderId) || folderId === "null") { return false; diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/body.ts b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts index d2ac7f36277..0508991c5da 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/body.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts @@ -16,16 +16,18 @@ const { css } = createEmotion({ export type NotificationConfirmationBodyProps = { buttonText: string; + itemName: string; confirmationMessage: string; error?: string; messageDetails?: string; tasksAreComplete?: boolean; theme: Theme; - handleOpenVault: (e: Event) => void; + handleOpenVault: () => void; }; export function NotificationConfirmationBody({ buttonText, + itemName, confirmationMessage, error, messageDetails, @@ -43,6 +45,7 @@ export function NotificationConfirmationBody({ ${showConfirmationMessage ? NotificationConfirmationMessage({ buttonText, + itemName, message: confirmationMessage, messageDetails, theme, diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/container.ts b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts index a071338af9a..5cc977cf4cb 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/container.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts @@ -20,14 +20,14 @@ import { NotificationConfirmationFooter } from "./footer"; export type NotificationConfirmationContainerProps = NotificationBarIframeInitData & { handleCloseNotification: (e: Event) => void; - handleOpenVault: (e: Event) => void; + handleOpenVault: () => void; handleOpenTasks: (e: Event) => void; } & { error?: string; i18n: { [key: string]: string }; + itemName: string; task?: NotificationTaskInfo; type: NotificationType; - username: string; }; export function NotificationConfirmationContainer({ @@ -36,13 +36,13 @@ export function NotificationConfirmationContainer({ handleOpenVault, handleOpenTasks, i18n, + itemName, task, theme = ThemeTypes.Light, type, - username, }: NotificationConfirmationContainerProps) { const headerMessage = getHeaderMessage(i18n, type, error); - const confirmationMessage = getConfirmationMessage(i18n, username, type, error); + const confirmationMessage = getConfirmationMessage(i18n, itemName, type, error); const buttonText = error ? i18n.newItem : i18n.view; let messageDetails: string | undefined; @@ -71,6 +71,7 @@ export function NotificationConfirmationContainer({ })} ${NotificationConfirmationBody({ buttonText, + itemName, confirmationMessage, tasksAreComplete, messageDetails, @@ -106,19 +107,17 @@ const notificationContainerStyles = (theme: Theme) => css` function getConfirmationMessage( i18n: { [key: string]: string }, - username: string, + itemName: string, type?: NotificationType, error?: string, ) { - const loginSaveSuccessDetails = chrome.i18n.getMessage("loginSaveSuccessDetails", [username]); - const loginUpdatedSuccessDetails = chrome.i18n.getMessage("loginUpdatedSuccessDetails", [ - username, - ]); + const loginSaveConfirmation = chrome.i18n.getMessage("loginSaveConfirmation", [itemName]); + const loginUpdatedConfirmation = chrome.i18n.getMessage("loginUpdatedConfirmation", [itemName]); if (error) { return i18n.saveFailureDetails; } - return type === "add" ? loginSaveSuccessDetails : loginUpdatedSuccessDetails; + return type === "add" ? loginSaveConfirmation : loginUpdatedConfirmation; } function getHeaderMessage( diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts index c018371caff..3707e628370 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts @@ -7,19 +7,23 @@ import { themes, typography } from "../../constants/styles"; export type NotificationConfirmationMessageProps = { buttonText?: string; + itemName: string; message?: string; messageDetails?: string; - handleClick: (e: Event) => void; + handleClick: () => void; theme: Theme; }; export function NotificationConfirmationMessage({ buttonText, + itemName, message, messageDetails, handleClick, theme, }: NotificationConfirmationMessageProps) { + const buttonAria = chrome.i18n.getMessage("notificationViewAria", [itemName]); + return html`
${message || buttonText @@ -35,6 +39,10 @@ export function NotificationConfirmationMessage({ title=${buttonText} class=${notificationConfirmationButtonTextStyles(theme)} @click=${handleClick} + @keydown=${(e: KeyboardEvent) => handleButtonKeyDown(e, handleClick)} + aria-label=${buttonAria} + tabindex="0" + role="button" > ${buttonText} @@ -81,3 +89,10 @@ const AdditionalMessageStyles = ({ theme }: { theme: Theme }) => css` font-size: 14px; color: ${themes[theme].text.muted}; `; + +function handleButtonKeyDown(event: KeyboardEvent, handleClick: () => void) { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + handleClick(); + } +} diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts index cbfeffcf2f4..9dd02b64154 100644 --- a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -33,7 +33,7 @@ type NotificationBarWindowMessage = { data?: { cipherId?: string; task?: NotificationTaskInfo; - username?: string; + itemName?: string; }; error?: string; initData?: NotificationBarIframeInitData; diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index 4e85d893178..fce05913e5e 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -56,9 +56,9 @@ function getI18n() { collection: chrome.i18n.getMessage("collection"), folder: chrome.i18n.getMessage("folder"), loginSaveSuccess: chrome.i18n.getMessage("loginSaveSuccess"), - loginSaveSuccessDetails: chrome.i18n.getMessage("loginSaveSuccessDetails"), + loginSaveConfirmation: chrome.i18n.getMessage("loginSaveConfirmation"), loginUpdateSuccess: chrome.i18n.getMessage("loginUpdateSuccess"), - loginUpdateSuccessDetails: chrome.i18n.getMessage("loginUpdatedSuccessDetails"), + loginUpdateConfirmation: chrome.i18n.getMessage("loginUpdatedConfirmation"), loginUpdateTaskSuccess: chrome.i18n.getMessage("loginUpdateTaskSuccess"), loginUpdateTaskSuccessAdditional: chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional"), nextSecurityTaskAction: chrome.i18n.getMessage("nextSecurityTaskAction"), @@ -72,6 +72,7 @@ function getI18n() { notificationEdit: chrome.i18n.getMessage("edit"), notificationUnlock: chrome.i18n.getMessage("notificationUnlock"), notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"), + notificationViewAria: chrome.i18n.getMessage("notificationViewAria"), saveAction: chrome.i18n.getMessage("notificationAddSave"), saveAsNewLoginAction: chrome.i18n.getMessage("saveAsNewLoginAction"), saveFailure: chrome.i18n.getMessage("saveFailure"), @@ -349,10 +350,9 @@ function handleSaveCipherAttemptCompletedMessage(message: NotificationBarWindowM ); } -function openViewVaultItemPopout(e: Event, cipherId: string) { - e.preventDefault(); +function openViewVaultItemPopout(cipherId: string) { sendPlatformMessage({ - command: "bgOpenVault", + command: "bgOpenViewVaultItemPopout", cipherId, }); } @@ -360,7 +360,7 @@ function openViewVaultItemPopout(e: Event, cipherId: string) { function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) { const { theme, type } = notificationBarIframeInitData; const { error, data } = message; - const { username, cipherId, task } = data || {}; + const { cipherId, task, itemName } = data || {}; const i18n = getI18n(); const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light); @@ -374,9 +374,9 @@ function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) { handleCloseNotification, i18n, error, - username: username ?? i18n.typeLogin, + itemName: itemName ?? i18n.typeLogin, task, - handleOpenVault: (e) => cipherId && openViewVaultItemPopout(e, cipherId), + handleOpenVault: () => cipherId && openViewVaultItemPopout(cipherId), handleOpenTasks: () => sendPlatformMessage({ command: "bgOpenAtRisksPasswords" }), }), document.body, From fe3e6fd1985c0b39a3110300713f41267e2b262c Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 23 Apr 2025 15:26:53 -0700 Subject: [PATCH 8/8] migrate to tw class name (#14317) --- .../components/autofill-options/uri-option.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html index 5301e4f32b9..ec98595e93b 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html +++ b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html @@ -1,5 +1,5 @@ -
+
{{ uriLabel }}