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] [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";