1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-11 10:10:25 +01:00

[PM-7900] Login Credentials and Autofill Browser V2 View sections (#10417)

* Added sections for Login Credentials and Autofill Options.
This commit is contained in:
Jason Ng 2024-08-08 13:52:45 -04:00 committed by GitHub
parent ad3c680f2c
commit bca619d0a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 289 additions and 6 deletions

View File

@ -22,9 +22,11 @@ import {
DialogService, DialogService,
ToastService, ToastService,
} from "@bitwarden/components"; } from "@bitwarden/components";
import { TotpCaptureService } from "@bitwarden/vault";
import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view"; import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view";
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component"; 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 { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "./../../../../../platform/popup/layout/popup-header.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", selector: "app-view-v2",
templateUrl: "view-v2.component.html", templateUrl: "view-v2.component.html",
standalone: true, standalone: true,
providers: [{ provide: TotpCaptureService, useClass: BrowserTotpCaptureService }],
imports: [ imports: [
CommonModule, CommonModule,
SearchModule, SearchModule,

View File

@ -13,10 +13,15 @@ describe("BrowserTotpCaptureService", () => {
let testBed: TestBed; let testBed: TestBed;
let service: BrowserTotpCaptureService; let service: BrowserTotpCaptureService;
let mockCaptureVisibleTab: jest.SpyInstance; let mockCaptureVisibleTab: jest.SpyInstance;
let createNewTabSpy: jest.SpyInstance;
const validTotpUrl = "otpauth://totp/label?secret=123"; const validTotpUrl = "otpauth://totp/label?secret=123";
beforeEach(() => { beforeEach(() => {
const tabReturn = new Promise<chrome.tabs.Tab>((resolve) =>
resolve({ url: "google.com", active: true } as chrome.tabs.Tab),
);
createNewTabSpy = jest.spyOn(BrowserApi, "createNewTab").mockReturnValue(tabReturn);
mockCaptureVisibleTab = jest.spyOn(BrowserApi, "captureVisibleTab"); mockCaptureVisibleTab = jest.spyOn(BrowserApi, "captureVisibleTab");
mockCaptureVisibleTab.mockResolvedValue("screenshot"); mockCaptureVisibleTab.mockResolvedValue("screenshot");
@ -66,4 +71,10 @@ describe("BrowserTotpCaptureService", () => {
expect(result).toBeNull(); 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");
});
}); });

View File

@ -20,4 +20,8 @@ export class BrowserTotpCaptureService implements TotpCaptureService {
} }
return null; return null;
} }
async openAutofillNewTab(loginUri: string) {
await BrowserApi.createNewTab(loginUri);
}
} }

View File

@ -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. * 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. * Captures a TOTP secret and returns it as a string. Returns null if no TOTP secret was found.
*/ */
abstract captureTotpSecret(): Promise<string | null>; abstract captureTotpSecret(): Promise<string | null>;
abstract openAutofillNewTab(loginUri: string): void;
} }

View File

@ -0,0 +1,30 @@
<bit-section>
<bit-section-header>
<h2 bitTypography="h6">{{ "autofillOptions" | i18n }}</h2>
</bit-section-header>
<bit-card>
<ng-container *ngFor="let login of loginUris; let last = last">
<bit-form-field [disableMargin]="last" data-testid="autofill-view-list">
<bit-label>
{{ "website" | i18n }}
</bit-label>
<input readonly bitInput type="text" [value]="login.launchUri" aria-readonly="true" />
<button
bitIconButton="bwi-external-link"
bitSuffix
type="button"
(click)="openWebsite(login.launchUri)"
></button>
<button
bitIconButton="bwi-clone"
bitSuffix
type="button"
[appCopyClick]="login.launchUri"
[valueLabel]="'website' | i18n"
showToast
[appA11yTitle]="'copyValue' | i18n"
></button>
</bit-form-field>
</ng-container>
</bit-card>
</bit-section>

View File

@ -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);
}
}

View File

@ -13,8 +13,6 @@ import {
IconButtonModule, IconButtonModule,
} from "@bitwarden/components"; } from "@bitwarden/components";
import { OrgIconDirective } from "../../components/org-icon.directive";
@Component({ @Component({
selector: "app-card-details-view", selector: "app-card-details-view",
templateUrl: "card-details-view.component.html", templateUrl: "card-details-view.component.html",
@ -26,7 +24,6 @@ import { OrgIconDirective } from "../../components/org-icon.directive";
SectionComponent, SectionComponent,
SectionHeaderComponent, SectionHeaderComponent,
TypographyModule, TypographyModule,
OrgIconDirective,
FormFieldModule, FormFieldModule,
IconButtonModule, IconButtonModule,
], ],

View File

@ -8,10 +8,19 @@
> >
</app-item-details-v2> </app-item-details-v2>
<!-- LOGIN CREDENTIALS -->
<app-login-credentials-view
*ngIf="hasLogin"
[login]="cipher.login"
[viewPassword]="cipher.viewPassword"
></app-login-credentials-view>
<!-- AUTOFILL OPTIONS -->
<app-autofill-options-view *ngIf="hasAutofill" [loginUris]="cipher.login.uris">
</app-autofill-options-view>
<!-- CARD DETAILS --> <!-- CARD DETAILS -->
<ng-container *ngIf="hasCard"> <app-card-details-view *ngIf="hasCard" [card]="cipher.card"></app-card-details-view>
<app-card-details-view [card]="cipher.card"></app-card-details-view>
</ng-container>
<!-- IDENTITY SECTIONS --> <!-- IDENTITY SECTIONS -->
<app-view-identity-sections *ngIf="cipher.identity" [cipher]="cipher"> <app-view-identity-sections *ngIf="cipher.identity" [cipher]="cipher">

View File

@ -19,10 +19,12 @@ import { PopupPageComponent } from "../../../../apps/browser/src/platform/popup/
import { AdditionalOptionsComponent } from "./additional-options/additional-options.component"; import { AdditionalOptionsComponent } from "./additional-options/additional-options.component";
import { AttachmentsV2ViewComponent } from "./attachments/attachments-v2-view.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 { CardDetailsComponent } from "./card-details/card-details-view.component";
import { CustomFieldV2Component } from "./custom-fields/custom-fields-v2.component"; import { CustomFieldV2Component } from "./custom-fields/custom-fields-v2.component";
import { ItemDetailsV2Component } from "./item-details/item-details-v2.component"; import { ItemDetailsV2Component } from "./item-details/item-details-v2.component";
import { ItemHistoryV2Component } from "./item-history/item-history-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"; import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-identity-sections.component";
@Component({ @Component({
@ -43,6 +45,8 @@ import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-ide
CustomFieldV2Component, CustomFieldV2Component,
CardDetailsComponent, CardDetailsComponent,
ViewIdentitySectionsComponent, ViewIdentitySectionsComponent,
LoginCredentialsViewComponent,
AutofillOptionsViewComponent,
], ],
}) })
export class CipherViewComponent implements OnInit, OnDestroy { export class CipherViewComponent implements OnInit, OnDestroy {
@ -61,6 +65,7 @@ export class CipherViewComponent implements OnInit, OnDestroy {
async ngOnInit() { async ngOnInit() {
await this.loadCipherData(); await this.loadCipherData();
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroyed$.next(); this.destroyed$.next();
this.destroyed$.complete(); this.destroyed$.complete();
@ -71,6 +76,15 @@ export class CipherViewComponent implements OnInit, OnDestroy {
return cardholderName || code || expMonth || expYear || brand || number; 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() { async loadCipherData() {
if (this.cipher.collectionIds.length > 0) { if (this.cipher.collectionIds.length > 0) {
this.collections$ = this.collectionService this.collections$ = this.collectionService

View File

@ -0,0 +1,106 @@
<bit-section>
<bit-section-header>
<h2 bitTypography="h6">{{ "loginCredentials" | i18n }}</h2>
</bit-section-header>
<bit-card>
<bit-form-field [disableMargin]="!login.password && !login.totp">
<bit-label>
{{ "username" | i18n }}
</bit-label>
<input
readonly
bitInput
type="text"
[value]="login.username"
aria-readonly="true"
data-testid="login-username"
/>
<button
bitIconButton="bwi-clone"
bitSuffix
type="button"
[appCopyClick]="login.username"
[valueLabel]="'username' | i18n"
showToast
[appA11yTitle]="'copyValue' | i18n"
data-testid="toggle-username"
></button>
</bit-form-field>
<bit-form-field [disableMargin]="!login.totp">
<bit-label>{{ "password" | i18n }}</bit-label>
<input
readonly
bitInput
type="password"
[value]="login.password"
aria-readonly="true"
data-testid="login-password"
/>
<button
bitSuffix
type="button"
bitIconButton
bitPasswordInputToggle
data-testid="toggle-password"
(toggledChange)="pwToggleValue($event)"
></button>
<button
*ngIf="viewPassword && passwordRevealed"
bitIconButton="bwi-numbered-list"
bitSuffix
type="button"
data-testid="toggle-password-count"
[appA11yTitle]="'toggleCharacterCount' | i18n"
appStopClick
(click)="togglePasswordCount()"
></button>
<button
bitIconButton="bwi-clone"
bitSuffix
type="button"
[appCopyClick]="login.password"
[valueLabel]="'password' | i18n"
showToast
[appA11yTitle]="'copyValue' | i18n"
data-testid="copy-password"
></button>
</bit-form-field>
<ng-container *ngIf="showPasswordCount && passwordRevealed">
<bit-color-password [password]="login.password" [showCount]="true"></bit-color-password>
</ng-container>
<bit-form-field disableMargin *ngIf="login.totp">
<bit-label
>{{ "verificationCodeTotp" | i18n }}
<span
*ngIf="!(isPremium$ | async)"
bitBadge
variant="success"
class="tw-ml-2"
(click)="getPremium()"
>
{{ "premium" | i18n }}
</span>
</bit-label>
<input
readonly
bitInput
type="text"
[value]="login.totp"
aria-readonly="true"
data-testid="login-totp"
[disabled]="!(isPremium$ | async)"
/>
<button
bitIconButton="bwi-clone"
bitSuffix
type="button"
[appCopyClick]="login.totp"
[valueLabel]="'verificationCodeTotp' | i18n"
showToast
[appA11yTitle]="'copyValue' | i18n"
data-testid="copy-totp"
[disabled]="!(isPremium$ | async)"
></button>
</bit-form-field>
</bit-card>
</bit-section>

View File

@ -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<boolean> =
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;
}
}