1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +00:00

[PM-18485] Remove new device verification guard (#14417)

* remove NewDeviceVerificationGuard and all associated entities. New Device verification feature has rolled out in production, this guard is no longer needed.

* remove unused properties from the vault profile service
This commit is contained in:
Nick Krantz
2025-05-06 13:08:30 -05:00
committed by GitHub
parent 5176345584
commit 46df5279a3
28 changed files with 8 additions and 1774 deletions

View File

@@ -1,33 +0,0 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<p class="tw-text-center" bitTypography="body1">
{{ "newDeviceVerificationNoticeContentPage1" | i18n }}
<a bitLink (click)="navigateToNewDeviceVerificationHelp($event)" href="#">
{{ "learnMore" | i18n }}.
</a>
</p>
<bit-card
class="tw-pb-0"
[ngClass]="{
'tw-flex tw-flex-col tw-items-center !tw-rounded-b-none': isDesktop,
'md:tw-flex md:tw-flex-col md:tw-items-center md:!tw-rounded-b-none': !isDesktop,
}"
>
<p bitTypography="body2" class="tw-text-muted md:tw-w-9/12">
{{ "newDeviceVerificationNoticePageOneFormContent" | i18n: this.currentEmail }}
</p>
<bit-radio-group formControlName="hasEmailAccess" class="md:tw-w-9/12">
<bit-radio-button id="option_A" [value]="0">
<bit-label>{{ "newDeviceVerificationNoticePageOneEmailAccessNo" | i18n }}</bit-label>
</bit-radio-button>
<bit-radio-button id="option_B" [value]="1">
<bit-label>{{ "newDeviceVerificationNoticePageOneEmailAccessYes" | i18n }}</bit-label>
</bit-radio-button>
</bit-radio-group>
</bit-card>
<button bitButton type="submit" buttonType="primary" class="tw-w-full tw-mt-4">
{{ "continue" | i18n }}
</button>
</form>

View File

@@ -1,173 +0,0 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { Router } from "@angular/router";
import { BehaviorSubject } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { NewDeviceVerificationNoticeService } from "../../services/new-device-verification-notice.service";
import { NewDeviceVerificationNoticePageOneComponent } from "./new-device-verification-notice-page-one.component";
describe("NewDeviceVerificationNoticePageOneComponent", () => {
let component: NewDeviceVerificationNoticePageOneComponent;
let fixture: ComponentFixture<NewDeviceVerificationNoticePageOneComponent>;
const activeAccount$ = new BehaviorSubject({ email: "test@example.com", id: "acct-1" });
const navigate = jest.fn().mockResolvedValue(null);
const updateNewDeviceVerificationNoticeState = jest.fn().mockResolvedValue(null);
const getFeatureFlag = jest.fn().mockResolvedValue(null);
beforeEach(async () => {
navigate.mockClear();
updateNewDeviceVerificationNoticeState.mockClear();
getFeatureFlag.mockClear();
await TestBed.configureTestingModule({
providers: [
{ provide: I18nService, useValue: { t: (...key: string[]) => key.join(" ") } },
{ provide: Router, useValue: { navigate } },
{ provide: AccountService, useValue: { activeAccount$ } },
{
provide: NewDeviceVerificationNoticeService,
useValue: { updateNewDeviceVerificationNoticeState },
},
{ provide: PlatformUtilsService, useValue: { getClientType: () => false } },
{ provide: ConfigService, useValue: { getFeatureFlag } },
],
}).compileComponents();
fixture = TestBed.createComponent(NewDeviceVerificationNoticePageOneComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("sets initial properties", () => {
expect(component["currentEmail"]).toBe("test@example.com");
expect(component["currentUserId"]).toBe("acct-1");
});
describe("temporary flag submission", () => {
beforeEach(() => {
getFeatureFlag.mockImplementation((key) => {
if (key === FeatureFlag.NewDeviceVerificationTemporaryDismiss) {
return Promise.resolve(true);
}
return Promise.resolve(false);
});
});
describe("no email access", () => {
beforeEach(() => {
component["formGroup"].controls.hasEmailAccess.setValue(0);
fixture.detectChanges();
const submit = fixture.debugElement.query(By.css('button[type="submit"]'));
submit.nativeElement.click();
});
it("redirects to step two ", () => {
expect(navigate).toHaveBeenCalledTimes(1);
expect(navigate).toHaveBeenCalledWith(["new-device-notice/setup"]);
});
it("does not update notice state", () => {
expect(getFeatureFlag).not.toHaveBeenCalled();
expect(updateNewDeviceVerificationNoticeState).not.toHaveBeenCalled();
});
});
describe("has email access", () => {
beforeEach(() => {
component["formGroup"].controls.hasEmailAccess.setValue(1);
fixture.detectChanges();
jest.useFakeTimers();
jest.setSystemTime(new Date("2024-03-03T00:00:00.000Z"));
const submit = fixture.debugElement.query(By.css('button[type="submit"]'));
submit.nativeElement.click();
});
afterEach(() => {
jest.useRealTimers();
});
it("redirects to the vault", () => {
expect(navigate).toHaveBeenCalledTimes(1);
expect(navigate).toHaveBeenCalledWith(["/vault"]);
});
it("updates notice state with a new date", () => {
expect(updateNewDeviceVerificationNoticeState).toHaveBeenCalledWith("acct-1", {
last_dismissal: new Date("2024-03-03T00:00:00.000Z"),
permanent_dismissal: false,
});
});
});
});
describe("permanent flag submission", () => {
beforeEach(() => {
getFeatureFlag.mockImplementation((key) => {
if (key === FeatureFlag.NewDeviceVerificationPermanentDismiss) {
return Promise.resolve(true);
}
return Promise.resolve(false);
});
});
describe("no email access", () => {
beforeEach(() => {
component["formGroup"].controls.hasEmailAccess.setValue(0);
fixture.detectChanges();
const submit = fixture.debugElement.query(By.css('button[type="submit"]'));
submit.nativeElement.click();
});
it("redirects to step two", () => {
expect(navigate).toHaveBeenCalledTimes(1);
expect(navigate).toHaveBeenCalledWith(["new-device-notice/setup"]);
});
it("does not update notice state", () => {
expect(getFeatureFlag).not.toHaveBeenCalled();
expect(updateNewDeviceVerificationNoticeState).not.toHaveBeenCalled();
});
});
describe("has email access", () => {
beforeEach(() => {
component["formGroup"].controls.hasEmailAccess.setValue(1);
fixture.detectChanges();
jest.useFakeTimers();
jest.setSystemTime(new Date("2024-04-04T00:00:00.000Z"));
const submit = fixture.debugElement.query(By.css('button[type="submit"]'));
submit.nativeElement.click();
});
afterEach(() => {
jest.useRealTimers();
});
it("redirects to the vault ", () => {
expect(navigate).toHaveBeenCalledTimes(1);
expect(navigate).toHaveBeenCalledWith(["/vault"]);
});
it("updates notice state with a new date", () => {
expect(updateNewDeviceVerificationNoticeState).toHaveBeenCalledWith("acct-1", {
last_dismissal: new Date("2024-04-04T00:00:00.000Z"),
permanent_dismissal: true,
});
});
});
});
});

View File

@@ -1,130 +0,0 @@
import { LiveAnnouncer } from "@angular/cdk/a11y";
import { CommonModule } from "@angular/common";
import { AfterViewInit, Component, OnInit } from "@angular/core";
import { FormBuilder, FormControl, ReactiveFormsModule } from "@angular/forms";
import { Router } from "@angular/router";
import { firstValueFrom, Observable } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ClientType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserId } from "@bitwarden/common/types/guid";
import {
AsyncActionsModule,
ButtonModule,
CardComponent,
FormFieldModule,
RadioButtonModule,
TypographyModule,
LinkModule,
} from "@bitwarden/components";
import {
NewDeviceVerificationNotice,
NewDeviceVerificationNoticeService,
} from "./../../services/new-device-verification-notice.service";
@Component({
standalone: true,
selector: "app-new-device-verification-notice-page-one",
templateUrl: "./new-device-verification-notice-page-one.component.html",
imports: [
CardComponent,
CommonModule,
JslibModule,
TypographyModule,
ButtonModule,
RadioButtonModule,
FormFieldModule,
AsyncActionsModule,
ReactiveFormsModule,
LinkModule,
],
})
export class NewDeviceVerificationNoticePageOneComponent implements OnInit, AfterViewInit {
protected formGroup = this.formBuilder.group({
hasEmailAccess: new FormControl(0),
});
protected isDesktop: boolean;
readonly currentAcct$: Observable<Account | null> = this.accountService.activeAccount$;
protected currentEmail: string = "";
private currentUserId: UserId | null = null;
constructor(
private formBuilder: FormBuilder,
private router: Router,
private accountService: AccountService,
private newDeviceVerificationNoticeService: NewDeviceVerificationNoticeService,
private platformUtilsService: PlatformUtilsService,
private configService: ConfigService,
private liveAnnouncer: LiveAnnouncer,
private i18nService: I18nService,
) {
this.isDesktop = this.platformUtilsService.getClientType() === ClientType.Desktop;
}
async ngOnInit() {
const currentAcct = await firstValueFrom(this.currentAcct$);
if (!currentAcct) {
return;
}
this.currentEmail = currentAcct.email;
this.currentUserId = currentAcct.id;
}
ngAfterViewInit() {
void this.liveAnnouncer.announce(this.i18nService.t("importantNotice"), "polite");
}
submit = async () => {
const doesNotHaveEmailAccess = this.formGroup.controls.hasEmailAccess.value === 0;
if (doesNotHaveEmailAccess) {
await this.router.navigate(["new-device-notice/setup"]);
return;
}
const tempNoticeFlag = await this.configService.getFeatureFlag(
FeatureFlag.NewDeviceVerificationTemporaryDismiss,
);
const permNoticeFlag = await this.configService.getFeatureFlag(
FeatureFlag.NewDeviceVerificationPermanentDismiss,
);
let newNoticeState: NewDeviceVerificationNotice | null = null;
// When the temporary flag is enabled, only update the `last_dismissal`
if (tempNoticeFlag) {
newNoticeState = {
last_dismissal: new Date(),
permanent_dismissal: false,
};
} else if (permNoticeFlag) {
// When the per flag is enabled, only update the `last_dismissal`
newNoticeState = {
last_dismissal: new Date(),
permanent_dismissal: true,
};
}
// This shouldn't occur as the user shouldn't get here unless one of the flags is active.
if (newNoticeState) {
await this.newDeviceVerificationNoticeService.updateNewDeviceVerificationNoticeState(
this.currentUserId!,
newNoticeState,
);
}
await this.router.navigate(["/vault"]);
};
navigateToNewDeviceVerificationHelp(event: Event) {
event.preventDefault();
this.platformUtilsService.launchUri("https://bitwarden.com/help/new-device-verification/");
}
}

View File

@@ -1,48 +0,0 @@
<p class="tw-text-center" bitTypography="body1">
{{ "newDeviceVerificationNoticeContentPage2" | i18n }}
</p>
<a
href="#"
bitButton
(click)="navigateToTwoStepLogin($event)"
buttonType="primary"
class="tw-w-full tw-mt-4"
data-testid="two-factor"
>
{{ "turnOnTwoStepLogin" | i18n }}
<i
class="bwi bwi-external-link bwi-lg bwi-fw"
aria-hidden="true"
[ngClass]="{ 'md:tw-hidden': !isDesktop }"
>
</i>
</a>
<a
href="#"
bitButton
(click)="navigateToChangeAcctEmail($event)"
buttonType="secondary"
class="tw-w-full tw-mt-4"
data-testid="change-email"
>
{{ "changeAcctEmail" | i18n }}
<i
class="bwi bwi-external-link bwi-lg bwi-fw"
aria-hidden="true"
[ngClass]="{ 'md:tw-hidden': !isDesktop }"
></i>
</a>
<div class="tw-flex tw-justify-center tw-mt-6" *ngIf="!permanentFlagEnabled">
<a
bitLink
linkType="primary"
(click)="remindMeLaterSelect()"
data-testid="remind-me-later"
href="#"
appStopClick
>
{{ "remindMeLater" | i18n }}
</a>
</div>

View File

@@ -1,175 +0,0 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { Router } from "@angular/router";
import { BehaviorSubject } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ClientType } from "@bitwarden/common/enums";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { NewDeviceVerificationNoticeService } from "../../services/new-device-verification-notice.service";
import { NewDeviceVerificationNoticePageTwoComponent } from "./new-device-verification-notice-page-two.component";
describe("NewDeviceVerificationNoticePageTwoComponent", () => {
let component: NewDeviceVerificationNoticePageTwoComponent;
let fixture: ComponentFixture<NewDeviceVerificationNoticePageTwoComponent>;
const activeAccount$ = new BehaviorSubject({ email: "test@example.com", id: "acct-1" });
const environment$ = new BehaviorSubject({ getWebVaultUrl: () => "vault.bitwarden.com" });
const navigate = jest.fn().mockResolvedValue(null);
const updateNewDeviceVerificationNoticeState = jest.fn().mockResolvedValue(null);
const getFeatureFlag = jest.fn().mockResolvedValue(false);
const getClientType = jest.fn().mockReturnValue(ClientType.Browser);
const launchUri = jest.fn();
beforeEach(async () => {
navigate.mockClear();
updateNewDeviceVerificationNoticeState.mockClear();
getFeatureFlag.mockClear();
getClientType.mockClear();
launchUri.mockClear();
await TestBed.configureTestingModule({
providers: [
{ provide: I18nService, useValue: { t: (...key: string[]) => key.join(" ") } },
{ provide: Router, useValue: { navigate } },
{ provide: AccountService, useValue: { activeAccount$ } },
{ provide: EnvironmentService, useValue: { environment$ } },
{
provide: NewDeviceVerificationNoticeService,
useValue: { updateNewDeviceVerificationNoticeState },
},
{ provide: PlatformUtilsService, useValue: { getClientType, launchUri } },
{ provide: ConfigService, useValue: { getFeatureFlag } },
],
}).compileComponents();
fixture = TestBed.createComponent(NewDeviceVerificationNoticePageTwoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("sets initial properties", () => {
expect(component["currentUserId"]).toBe("acct-1");
expect(component["permanentFlagEnabled"]).toBe(false);
});
describe("change email", () => {
const changeEmailButton = () =>
fixture.debugElement.query(By.css('[data-testid="change-email"]'));
describe("web", () => {
beforeEach(() => {
component["isWeb"] = true;
fixture.detectChanges();
});
it("navigates to settings", () => {
changeEmailButton().nativeElement.click();
expect(navigate).toHaveBeenCalledTimes(1);
expect(navigate).toHaveBeenCalledWith(["/settings/account"], {
queryParams: { fromNewDeviceVerification: true },
});
expect(launchUri).not.toHaveBeenCalled();
});
});
describe("browser/desktop", () => {
beforeEach(() => {
component["isWeb"] = false;
fixture.detectChanges();
});
it("launches to settings", () => {
changeEmailButton().nativeElement.click();
expect(navigate).not.toHaveBeenCalled();
expect(launchUri).toHaveBeenCalledWith(
"vault.bitwarden.com/#/settings/account/?fromNewDeviceVerification=true",
);
});
});
});
describe("enable 2fa", () => {
const changeEmailButton = () =>
fixture.debugElement.query(By.css('[data-testid="two-factor"]'));
describe("web", () => {
beforeEach(() => {
component["isWeb"] = true;
fixture.detectChanges();
});
it("navigates to two factor settings", () => {
changeEmailButton().nativeElement.click();
expect(navigate).toHaveBeenCalledTimes(1);
expect(navigate).toHaveBeenCalledWith(["/settings/security/two-factor"], {
queryParams: { fromNewDeviceVerification: true },
});
expect(launchUri).not.toHaveBeenCalled();
});
});
describe("browser/desktop", () => {
beforeEach(() => {
component["isWeb"] = false;
fixture.detectChanges();
});
it("launches to two factor settings", () => {
changeEmailButton().nativeElement.click();
expect(navigate).not.toHaveBeenCalled();
expect(launchUri).toHaveBeenCalledWith(
"vault.bitwarden.com/#/settings/security/two-factor/?fromNewDeviceVerification=true",
);
});
});
});
describe("remind me later", () => {
const remindMeLater = () =>
fixture.debugElement.query(By.css('[data-testid="remind-me-later"]'));
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date("2024-02-02T00:00:00.000Z"));
});
afterEach(() => {
jest.useRealTimers();
});
it("navigates to the vault", () => {
remindMeLater().nativeElement.click();
expect(navigate).toHaveBeenCalledTimes(1);
expect(navigate).toHaveBeenCalledWith(["/vault"]);
});
it("updates notice state", () => {
remindMeLater().nativeElement.click();
expect(updateNewDeviceVerificationNoticeState).toHaveBeenCalledTimes(1);
expect(updateNewDeviceVerificationNoticeState).toHaveBeenCalledWith("acct-1", {
last_dismissal: new Date("2024-02-02T00:00:00.000Z"),
permanent_dismissal: false,
});
});
it("is hidden when the permanent flag is enabled", async () => {
getFeatureFlag.mockResolvedValueOnce(true);
await component.ngOnInit();
fixture.detectChanges();
expect(remindMeLater()).toBeNull();
});
});
});

View File

@@ -1,111 +0,0 @@
import { LiveAnnouncer } from "@angular/cdk/a11y";
import { CommonModule } from "@angular/common";
import { AfterViewInit, Component, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom, Observable } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ClientType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import {
Environment,
EnvironmentService,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserId } from "@bitwarden/common/types/guid";
import { ButtonModule, LinkModule, TypographyModule } from "@bitwarden/components";
import { NewDeviceVerificationNoticeService } from "../../services/new-device-verification-notice.service";
@Component({
standalone: true,
selector: "app-new-device-verification-notice-page-two",
templateUrl: "./new-device-verification-notice-page-two.component.html",
imports: [CommonModule, JslibModule, TypographyModule, ButtonModule, LinkModule],
})
export class NewDeviceVerificationNoticePageTwoComponent implements OnInit, AfterViewInit {
protected isWeb: boolean;
protected isDesktop: boolean;
protected permanentFlagEnabled = false;
readonly currentAcct$: Observable<Account | null> = this.accountService.activeAccount$;
private currentUserId: UserId | null = null;
private env$: Observable<Environment> = this.environmentService.environment$;
constructor(
private newDeviceVerificationNoticeService: NewDeviceVerificationNoticeService,
private router: Router,
private accountService: AccountService,
private platformUtilsService: PlatformUtilsService,
private environmentService: EnvironmentService,
private configService: ConfigService,
private liveAnnouncer: LiveAnnouncer,
private i18nService: I18nService,
) {
this.isWeb = this.platformUtilsService.getClientType() === ClientType.Web;
this.isDesktop = this.platformUtilsService.getClientType() === ClientType.Desktop;
}
async ngOnInit() {
this.permanentFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.NewDeviceVerificationPermanentDismiss,
);
const currentAcct = await firstValueFrom(this.currentAcct$);
if (!currentAcct) {
return;
}
this.currentUserId = currentAcct.id;
}
ngAfterViewInit() {
void this.liveAnnouncer.announce(this.i18nService.t("setupTwoStepLogin"), "polite");
}
async navigateToTwoStepLogin(event: Event) {
event.preventDefault();
const env = await firstValueFrom(this.env$);
const url = env.getWebVaultUrl();
if (this.isWeb) {
await this.router.navigate(["/settings/security/two-factor"], {
queryParams: { fromNewDeviceVerification: true },
});
} else {
this.platformUtilsService.launchUri(
url + "/#/settings/security/two-factor/?fromNewDeviceVerification=true",
);
}
}
async navigateToChangeAcctEmail(event: Event) {
event.preventDefault();
const env = await firstValueFrom(this.env$);
const url = env.getWebVaultUrl();
if (this.isWeb) {
await this.router.navigate(["/settings/account"], {
queryParams: { fromNewDeviceVerification: true },
});
} else {
this.platformUtilsService.launchUri(
url + "/#/settings/account/?fromNewDeviceVerification=true",
);
}
}
async remindMeLaterSelect() {
await this.newDeviceVerificationNoticeService.updateNewDeviceVerificationNoticeState(
this.currentUserId!,
{
last_dismissal: new Date(),
permanent_dismissal: false,
},
);
await this.router.navigate(["/vault"]);
}
}

View File

@@ -1,7 +0,0 @@
import { svgIcon } from "@bitwarden/components";
export const ExclamationTriangle = svgIcon`
<svg width="120" height="100" viewBox="0 0 120 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M91.0871 85.1224H28.913C27.5592 85.1349 26.2271 84.7737 25.0549 84.0713C23.8828 83.3688 22.914 82.3578 22.248 81.1386C21.5868 79.9571 21.2405 78.6149 21.2502 77.2502C21.2599 75.8855 21.6207 74.5484 22.2964 73.3768L53.3835 18.7683C54.0665 17.5817 55.0352 16.6008 56.1953 15.9184C57.3554 15.2361 58.6656 14.8773 60 14.8773C61.3345 14.8773 62.6447 15.2361 63.8048 15.9184C64.9649 16.6008 65.9336 17.5817 66.6166 18.7683L97.7036 73.3768C98.3793 74.5484 98.7426 75.8855 98.7499 77.2502C98.7571 78.6149 98.4132 79.9571 97.7521 81.1386C97.0861 82.3578 96.1173 83.3713 94.9451 84.0713C93.773 84.7712 92.4409 85.1349 91.0871 85.1224ZM60 19.8972C59.5084 19.8896 59.0216 20.0176 58.5929 20.2659C58.1643 20.5143 57.8058 20.878 57.554 21.3171L26.4717 75.9256C26.2344 76.3371 26.1085 76.8087 26.1085 77.2878C26.1085 77.767 26.2344 78.2386 26.4717 78.65C26.7188 79.0991 27.0772 79.4704 27.5107 79.7263C27.9442 79.9821 28.4359 80.1126 28.9324 80.1051H91.0871C91.586 80.1126 92.0776 79.9821 92.5087 79.7263C92.9398 79.4704 93.3007 79.0991 93.5477 78.65C93.7851 78.2386 93.911 77.767 93.911 77.2878C93.911 76.8087 93.7851 76.3371 93.5477 75.9256L62.4461 21.3171C62.1943 20.878 61.8358 20.5168 61.4071 20.2659C60.9785 20.0151 60.4917 19.8896 60 19.8972ZM60 62.8705C59.3582 62.8705 58.7407 62.6071 58.2878 62.1355C57.8349 61.6639 57.5782 61.0267 57.5782 60.3619V37.4177C57.5782 36.7529 57.8325 36.1132 58.2878 35.644C58.7431 35.1749 59.3582 34.909 60 34.909C60.6418 34.909 61.2594 35.1724 61.7123 35.644C62.1652 36.1157 62.4219 36.7529 62.4219 37.4177V60.3619C62.4219 61.0267 62.1676 61.6664 61.7123 62.1355C61.257 62.6046 60.6418 62.8705 60 62.8705ZM60 75.2734C61.5864 75.2734 62.8724 73.9413 62.8724 72.2981C62.8724 70.6549 61.5864 69.3228 60 69.3228C58.4137 69.3228 57.1277 70.6549 57.1277 72.2981C57.1277 73.9413 58.4137 75.2734 60 75.2734Z" class="tw-fill-warning-600" />
</svg>
`;

View File

@@ -2,8 +2,6 @@ export * from "./deactivated-org";
export * from "./no-folders";
export * from "./vault";
export * from "./empty-trash";
export * from "./exclamation-triangle";
export * from "./user-lock";
export * from "./browser-extension";
export * from "./bitwarden-icon";
export * from "./security-handshake";

View File

@@ -1,17 +0,0 @@
import { svgIcon } from "@bitwarden/components";
export const UserLock = svgIcon`
<svg width="120" height="100" viewBox="0 0 120 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path class="tw-fill-art-primary" fill-rule="evenodd" clip-rule="evenodd" d="M0 19.6127C0 14.9806 3.71508 11.2256 8.29787 11.2256H89.5153C94.098 11.2256 97.8131 14.9806 97.8131 19.6127V28.0844H95.2599V19.6127C95.2599 16.4059 92.688 13.8062 89.5153 13.8062H8.29787C5.12517 13.8062 2.55319 16.4059 2.55319 19.6127V68.6449C2.55319 71.8518 5.12517 74.4514 8.29787 74.4514H16.2389V77.032H8.29787C3.71509 77.032 0 73.277 0 68.6449V19.6127ZM50.9015 74.4514H63.2653V77.032H50.9015V74.4514Z" />
<path class="tw-fill-art-primary" fill-rule="evenodd" clip-rule="evenodd" d="M88.7235 60.2578C97.5366 60.2578 104.681 53.0366 104.681 44.1287C104.681 35.2209 97.5366 27.9997 88.7235 27.9997C79.9105 27.9997 72.7661 35.2209 72.7661 44.1287C72.7661 53.0366 79.9105 60.2578 88.7235 60.2578ZM88.7235 62.8384C98.9467 62.8384 107.234 54.4618 107.234 44.1287C107.234 33.7957 98.9467 25.4191 88.7235 25.4191C78.5004 25.4191 70.2129 33.7957 70.2129 44.1287C70.2129 54.4618 78.5004 62.8384 88.7235 62.8384Z" />
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M33.8298 46.0642C28.7286 46.0642 24.2553 51.0589 24.2553 57.6771H21.7021C21.7021 50.0428 26.9452 43.4835 33.8298 43.4835C40.7144 43.4835 45.9575 50.0428 45.9575 57.6771H43.4043C43.4043 51.0589 38.931 46.0642 33.8298 46.0642Z" />
<path class="tw-fill-art-primary" fill-rule="evenodd" clip-rule="evenodd" d="M60.8742 88.6448H115.299C116.596 88.6448 117.089 88.3032 117.227 88.1603C117.3 88.0845 117.558 87.7852 117.396 86.7815C115.225 73.2724 103.259 62.8383 88.7118 62.8383C74.1651 62.8383 62.1986 73.2724 60.0274 86.7815C59.9451 87.2938 60.0532 87.888 60.2785 88.2731C60.379 88.445 60.4746 88.5285 60.5381 88.5682C60.5886 88.5998 60.6806 88.6448 60.8742 88.6448ZM60.8742 91.2254H115.299C118.653 91.2254 120.416 89.4793 119.916 86.3677C117.539 71.5724 104.473 60.2577 88.7118 60.2577C72.9505 60.2577 59.8851 71.5724 57.5073 86.3677C57.1687 88.4743 58.2526 91.2254 60.8742 91.2254Z" />
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M44.6808 58.9675H22.9787C20.1585 58.9675 17.8723 61.2783 17.8723 64.1288V79.6126C17.8723 82.4631 20.1585 84.7739 22.9787 84.7739H44.6808C47.501 84.7739 49.7872 82.4631 49.7872 79.6126V64.1288C49.7872 61.2783 47.501 58.9675 44.6808 58.9675ZM22.9787 56.3868C18.7484 56.3868 15.3191 59.853 15.3191 64.1288V79.6126C15.3191 83.8884 18.7484 87.3546 22.9787 87.3546H44.6808C48.9111 87.3546 52.3404 83.8884 52.3404 79.6126V64.1288C52.3404 59.853 48.9111 56.3868 44.6808 56.3868H22.9787Z"/>
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M33.8301 71.8707C34.5351 71.8707 35.1067 72.4484 35.1067 73.1611L35.1067 77.6923C35.1067 78.4049 34.5351 78.9826 33.8301 78.9826C33.125 78.9826 32.5535 78.4049 32.5535 77.6923L32.5535 73.1611C32.5535 72.4484 33.125 71.8707 33.8301 71.8707Z" />
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M78.315 18.3374C77.6099 18.3374 77.0384 17.7597 77.0384 17.0471V16.5436C77.0384 15.831 77.6099 15.2533 78.315 15.2533C79.02 15.2533 79.5916 15.831 79.5916 16.5436V17.0471C79.5916 17.7597 79.02 18.3374 78.315 18.3374Z" />
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M33.8299 74.3097C32.4198 74.3097 31.2767 73.1543 31.2767 71.729V71.2256C31.2767 69.8003 32.4198 68.6449 33.8299 68.6449C35.24 68.6449 36.3831 69.8003 36.3831 71.2256V71.729C36.3831 73.1543 35.24 74.3097 33.8299 74.3097Z" />
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M84.2903 18.3374C83.5853 18.3374 83.0137 17.7597 83.0137 17.0471V16.5436C83.0137 15.831 83.5853 15.2533 84.2903 15.2533C84.9953 15.2533 85.5669 15.831 85.5669 16.5436V17.0471C85.5669 17.7597 84.9953 18.3374 84.2903 18.3374Z" />
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M90.2644 18.3374C89.5594 18.3374 88.9878 17.7597 88.9878 17.0471V16.5436C88.9878 15.831 89.5594 15.2533 90.2644 15.2533C90.9695 15.2533 91.541 15.831 91.541 16.5436V17.0471C91.541 17.7597 90.9695 18.3374 90.2644 18.3374Z"/>
<path class="tw-fill-art-primary" fill-rule="evenodd" clip-rule="evenodd" d="M95.7422 22.0817H0.638428V20.7914H95.7422V22.0817Z" />
</svg>
`;

View File

@@ -1,5 +1,4 @@
export { PasswordRepromptService } from "./services/password-reprompt.service";
export { NewDeviceVerificationNoticeService } from "./services/new-device-verification-notice.service";
export { CopyCipherFieldService, CopyAction } from "./services/copy-cipher-field.service";
export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directive";
export { OrgIconDirective } from "./components/org-icon.directive";
@@ -16,8 +15,6 @@ export {
export { DownloadAttachmentComponent } from "./components/download-attachment/download-attachment.component";
export { PasswordHistoryViewComponent } from "./components/password-history-view/password-history-view.component";
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";

View File

@@ -1,112 +0,0 @@
import { firstValueFrom } from "rxjs";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import {
FakeAccountService,
FakeSingleUserState,
FakeStateProvider,
mockAccountServiceWith,
} from "../../../common/spec";
import {
NewDeviceVerificationNoticeService,
NewDeviceVerificationNotice,
NEW_DEVICE_VERIFICATION_NOTICE_KEY,
SKIP_NEW_DEVICE_VERIFICATION_NOTICE,
} from "./new-device-verification-notice.service";
describe("New Device Verification Notice", () => {
const sut = NEW_DEVICE_VERIFICATION_NOTICE_KEY;
const userId = Utils.newGuid() as UserId;
let newDeviceVerificationService: NewDeviceVerificationNoticeService;
let mockNoticeState: FakeSingleUserState<NewDeviceVerificationNotice>;
let mockSkipState: FakeSingleUserState<boolean>;
let stateProvider: FakeStateProvider;
let accountService: FakeAccountService;
beforeEach(() => {
accountService = mockAccountServiceWith(userId);
stateProvider = new FakeStateProvider(accountService);
mockNoticeState = stateProvider.singleUser.getFake(userId, NEW_DEVICE_VERIFICATION_NOTICE_KEY);
mockSkipState = stateProvider.singleUser.getFake(userId, SKIP_NEW_DEVICE_VERIFICATION_NOTICE);
newDeviceVerificationService = new NewDeviceVerificationNoticeService(stateProvider);
});
it("should deserialize newDeviceVerificationNotice values", async () => {
const currentDate = new Date();
const inputObj = {
last_dismissal: currentDate,
permanent_dismissal: false,
};
const expectedFolderData = {
last_dismissal: currentDate.toJSON(),
permanent_dismissal: false,
};
const result = sut.deserializer(JSON.parse(JSON.stringify(inputObj)));
expect(result).toEqual(expectedFolderData);
});
describe("notice$", () => {
it("emits new device verification notice state", async () => {
const currentDate = new Date();
const data = {
last_dismissal: currentDate,
permanent_dismissal: false,
};
await stateProvider.setUserState(NEW_DEVICE_VERIFICATION_NOTICE_KEY, data, userId);
const result = await firstValueFrom(newDeviceVerificationService.noticeState$(userId));
expect(result).toBe(data);
});
});
describe("update notice state", () => {
it("should update the date with a new value", async () => {
const currentDate = new Date();
const oldDate = new Date("11-11-2011");
const oldState = {
last_dismissal: oldDate,
permanent_dismissal: false,
};
const newState = {
last_dismissal: currentDate,
permanent_dismissal: true,
};
mockNoticeState.nextState(oldState);
await newDeviceVerificationService.updateNewDeviceVerificationNoticeState(userId, newState);
const result = await firstValueFrom(newDeviceVerificationService.noticeState$(userId));
expect(result).toEqual(newState);
});
});
describe("skipNotice state", () => {
it("emits skip notice state", async () => {
const shouldSkip = true;
await stateProvider.setUserState(SKIP_NEW_DEVICE_VERIFICATION_NOTICE, shouldSkip, userId);
const result = await firstValueFrom(newDeviceVerificationService.skipState$(userId));
expect(result).toBe(shouldSkip);
});
it("should update the skip notice state", async () => {
const initialSkipState = false;
const updatedSkipState = true;
mockSkipState.nextState(initialSkipState);
await newDeviceVerificationService.updateNewDeviceVerificationSkipNoticeState(
userId,
updatedSkipState,
);
const result = await firstValueFrom(newDeviceVerificationService.skipState$(userId));
expect(result).toBe(updatedSkipState);
});
});
});

View File

@@ -1,89 +0,0 @@
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { Jsonify } from "type-fest";
import {
StateProvider,
UserKeyDefinition,
NEW_DEVICE_VERIFICATION_NOTICE,
SingleUserState,
} from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
// This service checks when to show New Device Verification Notice to Users
// It will be a two phase approach and the values below will work with two different feature flags
// If a user dismisses the notice, use "last_dismissal" to wait 7 days before re-prompting
// permanent_dismissal will be checked if the user should never see the notice again
export class NewDeviceVerificationNotice {
last_dismissal: Date | null = null;
permanent_dismissal: boolean | null = null;
constructor(obj: Partial<NewDeviceVerificationNotice>) {
if (obj == null) {
return;
}
this.last_dismissal = obj.last_dismissal || null;
this.permanent_dismissal = obj.permanent_dismissal || null;
}
static fromJSON(obj: Jsonify<NewDeviceVerificationNotice>) {
return Object.assign(new NewDeviceVerificationNotice({}), obj);
}
}
export const NEW_DEVICE_VERIFICATION_NOTICE_KEY =
new UserKeyDefinition<NewDeviceVerificationNotice>(
NEW_DEVICE_VERIFICATION_NOTICE,
"noticeState",
{
deserializer: (obj: Jsonify<NewDeviceVerificationNotice>) =>
NewDeviceVerificationNotice.fromJSON(obj),
clearOn: [],
},
);
export const SKIP_NEW_DEVICE_VERIFICATION_NOTICE = new UserKeyDefinition<boolean>(
NEW_DEVICE_VERIFICATION_NOTICE,
"shouldSkip",
{
deserializer: (data: boolean) => data,
clearOn: ["logout"],
},
);
@Injectable()
export class NewDeviceVerificationNoticeService {
constructor(private stateProvider: StateProvider) {}
private noticeState(userId: UserId): SingleUserState<NewDeviceVerificationNotice> {
return this.stateProvider.getUser(userId, NEW_DEVICE_VERIFICATION_NOTICE_KEY);
}
noticeState$(userId: UserId): Observable<NewDeviceVerificationNotice | null> {
return this.noticeState(userId).state$;
}
async updateNewDeviceVerificationNoticeState(
userId: UserId,
newState: NewDeviceVerificationNotice,
): Promise<void> {
await this.noticeState(userId).update(() => {
return { ...newState };
});
}
private skipState(userId: UserId): SingleUserState<boolean> {
return this.stateProvider.getUser(userId, SKIP_NEW_DEVICE_VERIFICATION_NOTICE);
}
skipState$(userId: UserId): Observable<boolean | null> {
return this.skipState(userId).state$;
}
async updateNewDeviceVerificationSkipNoticeState(
userId: UserId,
shouldSkip: boolean,
): Promise<void> {
await this.skipState(userId).update(() => shouldSkip);
}
}