[PM-7900] Login Credentials and Autofill Browser V2 View sections (#10417)
* Added sections for Login Credentials and Autofill Options.
This commit is contained in:
parent
ad3c680f2c
commit
bca619d0a4
|
@ -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,
|
||||
|
|
|
@ -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<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.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");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,4 +20,8 @@ export class BrowserTotpCaptureService implements TotpCaptureService {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async openAutofillNewTab(loginUri: string) {
|
||||
await BrowserApi.createNewTab(loginUri);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<string | null>;
|
||||
abstract openAutofillNewTab(loginUri: string): void;
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
],
|
||||
|
|
|
@ -8,10 +8,19 @@
|
|||
>
|
||||
</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 -->
|
||||
<ng-container *ngIf="hasCard">
|
||||
<app-card-details-view [card]="cipher.card"></app-card-details-view>
|
||||
</ng-container>
|
||||
<app-card-details-view *ngIf="hasCard" [card]="cipher.card"></app-card-details-view>
|
||||
|
||||
<!-- IDENTITY SECTIONS -->
|
||||
<app-view-identity-sections *ngIf="cipher.identity" [cipher]="cipher">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue