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:
parent
ad3c680f2c
commit
bca619d0a4
@ -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,
|
||||||
|
@ -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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -20,4 +20,8 @@ export class BrowserTotpCaptureService implements TotpCaptureService {
|
|||||||
}
|
}
|
||||||
return null;
|
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.
|
* 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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
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,
|
||||||
],
|
],
|
||||||
|
@ -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">
|
||||||
|
@ -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
|
||||||
|
@ -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
Block a user