1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 01:33:33 +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"]);
}
}