mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-14347][PM-14348] New Device Verification Logic (#12451)
* add account created date to the account information * set permanent dismissal flag when the user selects that they can access their email * update the logic of device verification notice * add service to cache the profile creation date to avoid calling the API multiple times * update step one logic for new device verification + add tests * update step two logic for new device verification + add tests - remove remind me later link for permanent logic * migrate 2FA check to use the profile property rather than hitting the API directly. The API for 2FA providers is only available on web so it didn't work for browser & native. * remove unneeded account related changes - profile creation is used from other sources * remove obsolete test * store the profile id within the vault service * remove unused map * store the associated profile id so account for profile switching in the extension * add comment for temporary service and ticket number to remove * formatting * move up logic for feature flags
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,8 @@ 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
@@ -18,7 +20,10 @@ import {
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { NewDeviceVerificationNoticeService } from "./../../services/new-device-verification-notice.service";
|
||||
import {
|
||||
NewDeviceVerificationNotice,
|
||||
NewDeviceVerificationNoticeService,
|
||||
} from "./../../services/new-device-verification-notice.service";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -51,6 +56,7 @@ export class NewDeviceVerificationNoticePageOneComponent implements OnInit {
|
||||
private accountService: AccountService,
|
||||
private newDeviceVerificationNoticeService: NewDeviceVerificationNoticeService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.isDesktop = this.platformUtilsService.getClientType() === ClientType.Desktop;
|
||||
}
|
||||
@@ -65,18 +71,44 @@ export class NewDeviceVerificationNoticePageOneComponent implements OnInit {
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
if (this.formGroup.controls.hasEmailAccess.value === 0) {
|
||||
await this.router.navigate(["new-device-notice/setup"]);
|
||||
} else if (this.formGroup.controls.hasEmailAccess.value === 1) {
|
||||
await this.newDeviceVerificationNoticeService.updateNewDeviceVerificationNoticeState(
|
||||
this.currentUserId,
|
||||
{
|
||||
last_dismissal: new Date(),
|
||||
permanent_dismissal: false,
|
||||
},
|
||||
);
|
||||
const doesNotHaveEmailAccess = this.formGroup.controls.hasEmailAccess.value === 0;
|
||||
|
||||
await this.router.navigate(["/vault"]);
|
||||
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"]);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(click)="navigateToTwoStepLogin($event)"
|
||||
buttonType="primary"
|
||||
class="tw-w-full tw-mt-4"
|
||||
data-testid="two-factor"
|
||||
>
|
||||
{{ "turnOnTwoStepLogin" | i18n }}
|
||||
<i
|
||||
@@ -23,6 +24,7 @@
|
||||
(click)="navigateToChangeAcctEmail($event)"
|
||||
buttonType="secondary"
|
||||
class="tw-w-full tw-mt-4"
|
||||
data-testid="change-email"
|
||||
>
|
||||
{{ "changeAcctEmail" | i18n }}
|
||||
<i
|
||||
@@ -32,8 +34,8 @@
|
||||
></i>
|
||||
</a>
|
||||
|
||||
<div class="tw-flex tw-justify-center tw-mt-6">
|
||||
<a bitLink linkType="primary" (click)="remindMeLaterSelect()">
|
||||
<div class="tw-flex tw-justify-center tw-mt-6" *ngIf="!permanentFlagEnabled">
|
||||
<a bitLink linkType="primary" (click)="remindMeLaterSelect()" data-testid="remind-me-later">
|
||||
{{ "remindMeLater" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,8 @@ 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,
|
||||
@@ -25,6 +27,7 @@ import { NewDeviceVerificationNoticeService } from "../../services/new-device-ve
|
||||
export class NewDeviceVerificationNoticePageTwoComponent implements OnInit {
|
||||
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$;
|
||||
@@ -35,12 +38,17 @@ export class NewDeviceVerificationNoticePageTwoComponent implements OnInit {
|
||||
private accountService: AccountService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private environmentService: EnvironmentService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
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;
|
||||
@@ -83,7 +91,7 @@ export class NewDeviceVerificationNoticePageTwoComponent implements OnInit {
|
||||
|
||||
async remindMeLaterSelect() {
|
||||
await this.newDeviceVerificationNoticeService.updateNewDeviceVerificationNoticeState(
|
||||
this.currentUserId,
|
||||
this.currentUserId!,
|
||||
{
|
||||
last_dismissal: new Date(),
|
||||
permanent_dismissal: false,
|
||||
|
||||
Reference in New Issue
Block a user