diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index add2efed96..8039ac1865 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -22,9 +22,11 @@ import { DialogService, ToastService, } from "@bitwarden/components"; +import { TotpCaptureService } from "@bitwarden/vault"; import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view"; import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component"; +import { BrowserTotpCaptureService } from "../../../services/browser-totp-capture.service"; import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component"; import { PopupHeaderComponent } from "./../../../../../platform/popup/layout/popup-header.component"; @@ -34,6 +36,7 @@ import { PopupPageComponent } from "./../../../../../platform/popup/layout/popup selector: "app-view-v2", templateUrl: "view-v2.component.html", standalone: true, + providers: [{ provide: TotpCaptureService, useClass: BrowserTotpCaptureService }], imports: [ CommonModule, SearchModule, diff --git a/apps/browser/src/vault/popup/services/browser-totp-capture.service.spec.ts b/apps/browser/src/vault/popup/services/browser-totp-capture.service.spec.ts index 2c9afacffd..e790735dc5 100644 --- a/apps/browser/src/vault/popup/services/browser-totp-capture.service.spec.ts +++ b/apps/browser/src/vault/popup/services/browser-totp-capture.service.spec.ts @@ -13,10 +13,15 @@ describe("BrowserTotpCaptureService", () => { let testBed: TestBed; let service: BrowserTotpCaptureService; let mockCaptureVisibleTab: jest.SpyInstance; + let createNewTabSpy: jest.SpyInstance; const validTotpUrl = "otpauth://totp/label?secret=123"; beforeEach(() => { + const tabReturn = new Promise((resolve) => + resolve({ url: "google.com", active: true } as chrome.tabs.Tab), + ); + createNewTabSpy = jest.spyOn(BrowserApi, "createNewTab").mockReturnValue(tabReturn); mockCaptureVisibleTab = jest.spyOn(BrowserApi, "captureVisibleTab"); mockCaptureVisibleTab.mockResolvedValue("screenshot"); @@ -66,4 +71,10 @@ describe("BrowserTotpCaptureService", () => { expect(result).toBeNull(); }); + + it("should call BrowserApi.createNewTab with a given loginURI", async () => { + await service.openAutofillNewTab("www.google.com"); + + expect(createNewTabSpy).toHaveBeenCalledWith("www.google.com"); + }); }); diff --git a/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts b/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts index 3f8ba61ed3..8f93db45c0 100644 --- a/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts +++ b/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts @@ -20,4 +20,8 @@ export class BrowserTotpCaptureService implements TotpCaptureService { } return null; } + + async openAutofillNewTab(loginUri: string) { + await BrowserApi.createNewTab(loginUri); + } } diff --git a/libs/vault/src/cipher-form/abstractions/totp-capture.service.ts b/libs/vault/src/cipher-form/abstractions/totp-capture.service.ts index d6d9556586..ad0b03be82 100644 --- a/libs/vault/src/cipher-form/abstractions/totp-capture.service.ts +++ b/libs/vault/src/cipher-form/abstractions/totp-capture.service.ts @@ -1,3 +1,8 @@ +/** + * TODO: PM-10727 - Rename and Refactor this service + * This service is being used in both CipherForm and CipherView. Update this service to reflect that + */ + /** * Service to capture TOTP secret from a client application. */ @@ -6,4 +11,5 @@ export abstract class TotpCaptureService { * Captures a TOTP secret and returns it as a string. Returns null if no TOTP secret was found. */ abstract captureTotpSecret(): Promise; + abstract openAutofillNewTab(loginUri: string): void; } diff --git a/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.html b/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.html new file mode 100644 index 0000000000..65a80009ce --- /dev/null +++ b/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.html @@ -0,0 +1,30 @@ + + +

{{ "autofillOptions" | i18n }}

+
+ + + + + {{ "website" | i18n }} + + + + + + + +
diff --git a/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.ts b/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.ts new file mode 100644 index 0000000000..84f25a146c --- /dev/null +++ b/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.ts @@ -0,0 +1,40 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; +import { + CardComponent, + FormFieldModule, + SectionComponent, + SectionHeaderComponent, + TypographyModule, + IconButtonModule, +} from "@bitwarden/components"; + +import { TotpCaptureService } from "../../cipher-form"; + +@Component({ + selector: "app-autofill-options-view", + templateUrl: "autofill-options-view.component.html", + standalone: true, + imports: [ + CommonModule, + JslibModule, + CardComponent, + SectionComponent, + SectionHeaderComponent, + TypographyModule, + FormFieldModule, + IconButtonModule, + ], +}) +export class AutofillOptionsViewComponent { + @Input() loginUris: LoginUriView[]; + + constructor(private totpCaptureService: TotpCaptureService) {} + + async openWebsite(selectedUri: string) { + await this.totpCaptureService.openAutofillNewTab(selectedUri); + } +} diff --git a/libs/vault/src/cipher-view/card-details/card-details-view.component.ts b/libs/vault/src/cipher-view/card-details/card-details-view.component.ts index a3c55977a2..028417faf1 100644 --- a/libs/vault/src/cipher-view/card-details/card-details-view.component.ts +++ b/libs/vault/src/cipher-view/card-details/card-details-view.component.ts @@ -13,8 +13,6 @@ import { IconButtonModule, } from "@bitwarden/components"; -import { OrgIconDirective } from "../../components/org-icon.directive"; - @Component({ selector: "app-card-details-view", templateUrl: "card-details-view.component.html", @@ -26,7 +24,6 @@ import { OrgIconDirective } from "../../components/org-icon.directive"; SectionComponent, SectionHeaderComponent, TypographyModule, - OrgIconDirective, FormFieldModule, IconButtonModule, ], diff --git a/libs/vault/src/cipher-view/cipher-view.component.html b/libs/vault/src/cipher-view/cipher-view.component.html index 6046bb48fd..2a2a4ded05 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.html +++ b/libs/vault/src/cipher-view/cipher-view.component.html @@ -8,10 +8,19 @@ > + + + + + + + - - - + diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index d2ac096391..7900fddbe0 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -19,10 +19,12 @@ import { PopupPageComponent } from "../../../../apps/browser/src/platform/popup/ import { AdditionalOptionsComponent } from "./additional-options/additional-options.component"; import { AttachmentsV2ViewComponent } from "./attachments/attachments-v2-view.component"; +import { AutofillOptionsViewComponent } from "./autofill-options/autofill-options-view.component"; import { CardDetailsComponent } from "./card-details/card-details-view.component"; import { CustomFieldV2Component } from "./custom-fields/custom-fields-v2.component"; import { ItemDetailsV2Component } from "./item-details/item-details-v2.component"; import { ItemHistoryV2Component } from "./item-history/item-history-v2.component"; +import { LoginCredentialsViewComponent } from "./login-credentials/login-credentials-view.component"; import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-identity-sections.component"; @Component({ @@ -43,6 +45,8 @@ import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-ide CustomFieldV2Component, CardDetailsComponent, ViewIdentitySectionsComponent, + LoginCredentialsViewComponent, + AutofillOptionsViewComponent, ], }) export class CipherViewComponent implements OnInit, OnDestroy { @@ -61,6 +65,7 @@ export class CipherViewComponent implements OnInit, OnDestroy { async ngOnInit() { await this.loadCipherData(); } + ngOnDestroy(): void { this.destroyed$.next(); this.destroyed$.complete(); @@ -71,6 +76,15 @@ export class CipherViewComponent implements OnInit, OnDestroy { return cardholderName || code || expMonth || expYear || brand || number; } + get hasLogin() { + const { username, password, totp } = this.cipher.login; + return username || password || totp; + } + + get hasAutofill() { + return this.cipher.login?.uris.length > 0; + } + async loadCipherData() { if (this.cipher.collectionIds.length > 0) { this.collections$ = this.collectionService diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html new file mode 100644 index 0000000000..9ca983a5c9 --- /dev/null +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html @@ -0,0 +1,106 @@ + + +

{{ "loginCredentials" | i18n }}

+
+ + + + {{ "username" | i18n }} + + + + + + {{ "password" | i18n }} + + + + + + + + + + {{ "verificationCodeTotp" | i18n }} + + {{ "premium" | i18n }} + + + + + + +
diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts new file mode 100644 index 0000000000..15234556e9 --- /dev/null +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts @@ -0,0 +1,63 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { Router } from "@angular/router"; +import { Observable, shareReplay } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { + CardComponent, + FormFieldModule, + SectionComponent, + SectionHeaderComponent, + TypographyModule, + IconButtonModule, + BadgeModule, + ColorPasswordModule, +} from "@bitwarden/components"; + +@Component({ + selector: "app-login-credentials-view", + templateUrl: "login-credentials-view.component.html", + standalone: true, + imports: [ + CommonModule, + JslibModule, + CardComponent, + SectionComponent, + SectionHeaderComponent, + TypographyModule, + FormFieldModule, + IconButtonModule, + BadgeModule, + ColorPasswordModule, + ], +}) +export class LoginCredentialsViewComponent { + @Input() login: LoginView; + @Input() viewPassword: boolean; + isPremium$: Observable = + this.billingAccountProfileStateService.hasPremiumFromAnySource$.pipe( + shareReplay({ refCount: true, bufferSize: 1 }), + ); + showPasswordCount: boolean = false; + passwordRevealed: boolean = false; + + constructor( + private billingAccountProfileStateService: BillingAccountProfileStateService, + private router: Router, + ) {} + + async getPremium() { + await this.router.navigate(["/premium"]); + } + + pwToggleValue(evt: boolean) { + this.passwordRevealed = evt; + } + + togglePasswordCount() { + this.showPasswordCount = !this.showPasswordCount; + } +}