diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 6813a10bbc..4de26cb269 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -5135,42 +5135,6 @@ "beta": { "message": "Beta" }, - "importantNotice": { - "message": "Important notice" - }, - "setupTwoStepLogin": { - "message": "Set up two-step login" - }, - "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." - }, - "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." - }, - "remindMeLater": { - "message": "Remind me later" - }, - "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", - "placeholders": { - "email": { - "content": "$1", - "example": "your_name@email.com" - } - } - }, - "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" - }, - "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" - }, - "turnOnTwoStepLogin": { - "message": "Turn on two-step login" - }, - "changeAcctEmail": { - "message": "Change account email" - }, "extensionWidth": { "message": "Extension width" }, diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 45955506b9..3b271fb129 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -15,7 +15,6 @@ import { tdeDecryptionRequiredGuard, unauthGuardFn, } from "@bitwarden/angular/auth/guards"; -import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, @@ -43,11 +42,6 @@ import { VaultIcon, } from "@bitwarden/auth/angular"; import { LockComponent } from "@bitwarden/key-management-ui"; -import { - NewDeviceVerificationNoticePageOneComponent, - NewDeviceVerificationNoticePageTwoComponent, - VaultIcons, -} from "@bitwarden/vault"; import { fido2AuthGuard } from "../auth/guards/fido2-auth.guard"; import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component"; @@ -605,34 +599,6 @@ const routes: Routes = [ }, ], }, - { - path: "new-device-notice", - component: ExtensionAnonLayoutWrapperComponent, - canActivate: [], - children: [ - { - path: "", - component: NewDeviceVerificationNoticePageOneComponent, - data: { - pageIcon: VaultIcons.ExclamationTriangle, - pageTitle: { - key: "importantNotice", - }, - hideFooter: true, - }, - }, - { - path: "setup", - component: NewDeviceVerificationNoticePageTwoComponent, - data: { - pageIcon: VaultIcons.UserLock, - pageTitle: { - key: "setupTwoStepLogin", - }, - }, - }, - ], - }, { path: "tabs", component: TabsV2Component, @@ -650,7 +616,7 @@ const routes: Routes = [ { path: "vault", component: VaultV2Component, - canActivate: [authGuard, NewDeviceVerificationNoticeGuard], + canActivate: [authGuard], canDeactivate: [clearVaultStateGuard], data: { elevation: 0 } satisfies RouteDataProperties, }, diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 00463152a9..70c40ee8f6 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -15,7 +15,6 @@ import { unauthGuardFn, } from "@bitwarden/angular/auth/guards"; import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; -import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, @@ -44,11 +43,6 @@ import { } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { LockComponent } from "@bitwarden/key-management-ui"; -import { - NewDeviceVerificationNoticePageOneComponent, - NewDeviceVerificationNoticePageTwoComponent, - VaultIcons, -} from "@bitwarden/vault"; import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; @@ -108,40 +102,13 @@ const routes: Routes = [ }, } satisfies RouteDataProperties & AnonLayoutWrapperData, }, - { - path: "new-device-notice", - component: AnonLayoutWrapperComponent, - canActivate: [], - children: [ - { - path: "", - component: NewDeviceVerificationNoticePageOneComponent, - data: { - pageIcon: VaultIcons.ExclamationTriangle, - pageTitle: { - key: "importantNotice", - }, - }, - }, - { - path: "setup", - component: NewDeviceVerificationNoticePageTwoComponent, - data: { - pageIcon: VaultIcons.UserLock, - pageTitle: { - key: "setupTwoStepLogin", - }, - }, - }, - ], - }, ...featureFlaggedRoute({ defaultComponent: VaultComponent, flaggedComponent: VaultV2Component, featureFlag: FeatureFlag.PM18520_UpdateDesktopCipherForm, routeOptions: { path: "vault", - canActivate: [authGuard, NewDeviceVerificationNoticeGuard], + canActivate: [authGuard], }, }), { path: "accessibility-cookie", component: AccessibilityCookieComponent }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 50620c106f..4bae767834 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3689,63 +3689,6 @@ "fileSavedToDevice": { "message": "File saved to device. Manage from your device downloads." }, - "importantNotice": { - "message": "Important notice" - }, - "setupTwoStepLogin": { - "message": "Set up two-step login" - }, - "itemDetails": { - "message": "Item details" - }, - "itemName": { - "message": "Item name" - }, - "loginCredentials": { - "message": "Login credentials" - }, - "additionalOptions": { - "message": "Additional options" - }, - "itemHistory": { - "message": "Item history" - }, - "lastEdited": { - "message": "Last edited" - }, - "upload": { - "message": "Upload" - }, - "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." - }, - "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." - }, - "remindMeLater": { - "message": "Remind me later" - }, - "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", - "placeholders": { - "email": { - "content": "$1", - "example": "your_name@email.com" - } - } - }, - "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" - }, - "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" - }, - "turnOnTwoStepLogin": { - "message": "Turn on two-step login" - }, - "changeAcctEmail": { - "message": "Change account email" - }, "allowScreenshots": { "message": "Allow screen capture" }, diff --git a/apps/web/src/app/auth/recover-two-factor.component.spec.ts b/apps/web/src/app/auth/recover-two-factor.component.spec.ts index bf6d47e09e..5977d99452 100644 --- a/apps/web/src/app/auth/recover-two-factor.component.spec.ts +++ b/apps/web/src/app/auth/recover-two-factor.component.spec.ts @@ -16,7 +16,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { I18nPipe } from "@bitwarden/ui-common"; -import { NewDeviceVerificationNoticeService } from "@bitwarden/vault"; import { RecoverTwoFactorComponent } from "./recover-two-factor.component"; @@ -35,7 +34,6 @@ describe("RecoverTwoFactorComponent", () => { let mockConfigService: MockProxy; let mockLoginSuccessHandlerService: MockProxy; let mockLogService: MockProxy; - let mockNewDeviceVerificationNoticeService: MockProxy; beforeEach(() => { mockRouter = mock(); @@ -48,7 +46,6 @@ describe("RecoverTwoFactorComponent", () => { mockConfigService = mock(); mockLoginSuccessHandlerService = mock(); mockLogService = mock(); - mockNewDeviceVerificationNoticeService = mock(); TestBed.configureTestingModule({ declarations: [RecoverTwoFactorComponent], @@ -63,10 +60,6 @@ describe("RecoverTwoFactorComponent", () => { { provide: ConfigService, useValue: mockConfigService }, { provide: LoginSuccessHandlerService, useValue: mockLoginSuccessHandlerService }, { provide: LogService, useValue: mockLogService }, - { - provide: NewDeviceVerificationNoticeService, - useValue: mockNewDeviceVerificationNoticeService, - }, ], imports: [I18nPipe], // FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports @@ -102,9 +95,6 @@ describe("RecoverTwoFactorComponent", () => { title: "", message: mockI18nService.t("youHaveBeenLoggedIn"), }); - expect( - mockNewDeviceVerificationNoticeService.updateNewDeviceVerificationSkipNoticeState, - ).toHaveBeenCalledWith(authResult.userId, true); expect(mockRouter.navigate).toHaveBeenCalledWith(["/settings/security/two-factor"]); }); diff --git a/apps/web/src/app/auth/recover-two-factor.component.ts b/apps/web/src/app/auth/recover-two-factor.component.ts index 35aa1aab7c..08881b64f3 100644 --- a/apps/web/src/app/auth/recover-two-factor.component.ts +++ b/apps/web/src/app/auth/recover-two-factor.component.ts @@ -12,7 +12,6 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ToastService } from "@bitwarden/components"; -import { NewDeviceVerificationNoticeService } from "@bitwarden/vault"; @Component({ selector: "app-recover-two-factor", @@ -37,7 +36,6 @@ export class RecoverTwoFactorComponent implements OnInit { private toastService: ToastService, private loginSuccessHandlerService: LoginSuccessHandlerService, private logService: LogService, - private newDeviceVerificationNoticeService: NewDeviceVerificationNoticeService, ) {} async ngOnInit() { @@ -104,13 +102,6 @@ export class RecoverTwoFactorComponent implements OnInit { await this.loginSuccessHandlerService.run(authResult.userId); - // Before routing, set the state to skip the new device notification. This is a temporary - // fix and will be cleaned up in PM-18485. - await this.newDeviceVerificationNoticeService.updateNewDeviceVerificationSkipNoticeState( - authResult.userId, - true, - ); - await this.router.navigate(["/settings/security/two-factor"]); } catch (error) { // If login errors, redirect to login page per product. Don't show error diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 3d62a4c107..37da9a35f6 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -10,7 +10,6 @@ import { unauthGuardFn, activeAuthGuard, } from "@bitwarden/angular/auth/guards"; -import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, @@ -41,11 +40,7 @@ import { DeviceVerificationIcon, } from "@bitwarden/auth/angular"; import { LockComponent } from "@bitwarden/key-management-ui"; -import { - NewDeviceVerificationNoticePageOneComponent, - NewDeviceVerificationNoticePageTwoComponent, - VaultIcons, -} from "@bitwarden/vault"; +import { VaultIcons } from "@bitwarden/vault"; import { flagEnabled, Flags } from "../utils/flags"; @@ -586,37 +581,10 @@ const routes: Routes = [ }, ], }, - { - path: "new-device-notice", - component: AnonLayoutWrapperComponent, - canActivate: [], - children: [ - { - path: "", - component: NewDeviceVerificationNoticePageOneComponent, - data: { - pageIcon: VaultIcons.ExclamationTriangle, - pageTitle: { - key: "importantNotice", - }, - }, - }, - { - path: "setup", - component: NewDeviceVerificationNoticePageTwoComponent, - data: { - pageIcon: VaultIcons.UserLock, - pageTitle: { - key: "setupTwoStepLogin", - }, - }, - }, - ], - }, { path: "", component: UserLayoutComponent, - canActivate: [deepLinkGuard(), authGuard, NewDeviceVerificationNoticeGuard], + canActivate: [deepLinkGuard(), authGuard], children: [ { path: "vault", diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 0e829ffbd3..3b7ff5c7aa 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -10246,42 +10246,6 @@ } } }, - "importantNotice": { - "message": "Important notice" - }, - "setupTwoStepLogin": { - "message": "Set up two-step login" - }, - "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." - }, - "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." - }, - "remindMeLater": { - "message": "Remind me later" - }, - "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", - "placeholders": { - "email": { - "content": "$1", - "example": "your_name@email.com" - } - } - }, - "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" - }, - "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" - }, - "turnOnTwoStepLogin": { - "message": "Turn on two-step login" - }, - "changeAcctEmail": { - "message": "Change account email" - }, "removeMembers": { "message": "Remove members" }, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 5596e66b4d..d82df8574f 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -312,7 +312,7 @@ import { UserAsymmetricKeysRegenerationService, } from "@bitwarden/key-management"; import { SafeInjectionToken } from "@bitwarden/ui-common"; -import { NewDeviceVerificationNoticeService, PasswordRepromptService } from "@bitwarden/vault"; +import { PasswordRepromptService } from "@bitwarden/vault"; import { IndividualVaultExportService, IndividualVaultExportServiceAbstraction, @@ -1459,7 +1459,6 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultLoginDecryptionOptionsService, deps: [MessagingServiceAbstraction], }), - safeProvider(NewDeviceVerificationNoticeService), safeProvider({ provide: UserAsymmetricKeysRegenerationApiService, useClass: DefaultUserAsymmetricKeysRegenerationApiService, diff --git a/libs/angular/src/vault/guards/index.ts b/libs/angular/src/vault/guards/index.ts deleted file mode 100644 index 001a483237..0000000000 --- a/libs/angular/src/vault/guards/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./new-device-verification-notice.guard"; diff --git a/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts b/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts deleted file mode 100644 index 53e2cef0d0..0000000000 --- a/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts +++ /dev/null @@ -1,293 +0,0 @@ -import { TestBed } from "@angular/core/testing"; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from "@angular/router"; -import { BehaviorSubject } from "rxjs"; - -import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -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 { NewDeviceVerificationNoticeService } from "@bitwarden/vault"; - -import { VaultProfileService } from "../services/vault-profile.service"; - -import { NewDeviceVerificationNoticeGuard } from "./new-device-verification-notice.guard"; - -describe("NewDeviceVerificationNoticeGuard", () => { - const _state = Object.freeze({}) as RouterStateSnapshot; - const emptyRoute = Object.freeze({ queryParams: {} }) as ActivatedRouteSnapshot; - const eightDaysAgo = new Date(); - eightDaysAgo.setDate(eightDaysAgo.getDate() - 8); - - const account = { - id: "account-id", - } as unknown as Account; - - const activeAccount$ = new BehaviorSubject(account); - - const createUrlTree = jest.fn(); - const getFeatureFlag = jest.fn().mockImplementation((key) => { - if (key === FeatureFlag.NewDeviceVerificationTemporaryDismiss) { - return Promise.resolve(true); - } - - return Promise.resolve(false); - }); - const isSelfHost = jest.fn().mockReturnValue(false); - const getProfileTwoFactorEnabled = jest.fn().mockResolvedValue(false); - const noticeState$ = jest.fn().mockReturnValue(new BehaviorSubject(null)); - const skipState$ = jest.fn().mockReturnValue(new BehaviorSubject(null)); - const getProfileCreationDate = jest.fn().mockResolvedValue(eightDaysAgo); - const hasMasterPasswordAndMasterKeyHash = jest.fn().mockResolvedValue(true); - const getUserSSOBound = jest.fn().mockResolvedValue(false); - const getUserSSOBoundAdminOwner = jest.fn().mockResolvedValue(false); - - beforeEach(() => { - getFeatureFlag.mockClear(); - isSelfHost.mockClear(); - getProfileCreationDate.mockClear(); - getProfileTwoFactorEnabled.mockClear(); - createUrlTree.mockClear(); - hasMasterPasswordAndMasterKeyHash.mockClear(); - getUserSSOBound.mockClear(); - getUserSSOBoundAdminOwner.mockClear(); - skipState$.mockClear(); - - TestBed.configureTestingModule({ - providers: [ - { provide: Router, useValue: { createUrlTree } }, - { provide: ConfigService, useValue: { getFeatureFlag } }, - { provide: NewDeviceVerificationNoticeService, useValue: { noticeState$, skipState$ } }, - { provide: AccountService, useValue: { activeAccount$ } }, - { provide: PlatformUtilsService, useValue: { isSelfHost } }, - { provide: UserVerificationService, useValue: { hasMasterPasswordAndMasterKeyHash } }, - { - provide: VaultProfileService, - useValue: { - getProfileCreationDate, - getProfileTwoFactorEnabled, - getUserSSOBound, - getUserSSOBoundAdminOwner, - }, - }, - ], - }); - }); - - function newDeviceGuard(route?: ActivatedRouteSnapshot) { - // Run the guard within injection context so `inject` works as you'd expect - // Pass state object to make TypeScript happy - return TestBed.runInInjectionContext(async () => - NewDeviceVerificationNoticeGuard(route ?? emptyRoute, _state), - ); - } - - describe("fromNewDeviceVerification", () => { - const route = { - queryParams: { fromNewDeviceVerification: "true" }, - } as unknown as ActivatedRouteSnapshot; - - it("returns `true` when `fromNewDeviceVerification` is present", async () => { - expect(await newDeviceGuard(route)).toBe(true); - }); - - it("does not execute other logic", async () => { - // `fromNewDeviceVerification` param should exit early, - // not foolproof but a quick way to test that other logic isn't executed - await newDeviceGuard(route); - - expect(getFeatureFlag).not.toHaveBeenCalled(); - expect(isSelfHost).not.toHaveBeenCalled(); - expect(getProfileTwoFactorEnabled).not.toHaveBeenCalled(); - expect(getProfileCreationDate).not.toHaveBeenCalled(); - expect(hasMasterPasswordAndMasterKeyHash).not.toHaveBeenCalled(); - }); - }); - - describe("missing current account", () => { - afterAll(() => { - // reset `activeAccount$` observable - activeAccount$.next(account); - }); - - it("redirects to login when account is missing", async () => { - activeAccount$.next(null); - - await newDeviceGuard(); - - expect(createUrlTree).toHaveBeenCalledWith(["/login"]); - }); - }); - - it("returns `true` when 2FA is enabled", async () => { - getProfileTwoFactorEnabled.mockResolvedValueOnce(true); - - expect(await newDeviceGuard()).toBe(true); - }); - - it("returns `true` when the user is self hosted", async () => { - isSelfHost.mockReturnValueOnce(true); - - expect(await newDeviceGuard()).toBe(true); - }); - - it("returns `true` when the profile was created less than a week ago", async () => { - const sixDaysAgo = new Date(); - sixDaysAgo.setDate(sixDaysAgo.getDate() - 6); - - getProfileCreationDate.mockResolvedValueOnce(sixDaysAgo); - - expect(await newDeviceGuard()).toBe(true); - }); - - it("returns `true` when the profile service throws an error", async () => { - getProfileCreationDate.mockRejectedValueOnce(new Error("test")); - - expect(await newDeviceGuard()).toBe(true); - }); - - it("returns `true` when the skip state value is set to true", async () => { - skipState$.mockReturnValueOnce(new BehaviorSubject(true)); - - expect(await newDeviceGuard()).toBe(true); - expect(skipState$.mock.calls[0][0]).toBe("account-id"); - expect(skipState$.mock.calls.length).toBe(1); - }); - - describe("SSO bound", () => { - beforeEach(() => { - getFeatureFlag.mockImplementation((key) => { - if (key === FeatureFlag.NewDeviceVerificationPermanentDismiss) { - return Promise.resolve(true); - } - - return Promise.resolve(false); - }); - }); - - afterAll(() => { - getFeatureFlag.mockReturnValue(false); - }); - - it('returns "true" when the user is SSO bound and not an admin or owner', async () => { - getUserSSOBound.mockResolvedValueOnce(true); - getUserSSOBoundAdminOwner.mockResolvedValueOnce(false); - - expect(await newDeviceGuard()).toBe(true); - }); - - it('returns "true" when the user is an admin or owner of an SSO bound organization and has not logged in with their master password', async () => { - getUserSSOBound.mockResolvedValueOnce(true); - getUserSSOBoundAdminOwner.mockResolvedValueOnce(true); - hasMasterPasswordAndMasterKeyHash.mockResolvedValueOnce(false); - - expect(await newDeviceGuard()).toBe(true); - }); - - it("shows notice when the user is an admin or owner of an SSO bound organization and logged in with their master password", async () => { - getUserSSOBound.mockResolvedValueOnce(true); - getUserSSOBoundAdminOwner.mockResolvedValueOnce(true); - hasMasterPasswordAndMasterKeyHash.mockResolvedValueOnce(true); - - await newDeviceGuard(); - - expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]); - }); - - it("shows notice when the user that is not in an SSO bound organization", async () => { - getUserSSOBound.mockResolvedValueOnce(false); - getUserSSOBoundAdminOwner.mockResolvedValueOnce(false); - hasMasterPasswordAndMasterKeyHash.mockResolvedValueOnce(true); - - await newDeviceGuard(); - - expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]); - }); - }); - - describe("temp flag", () => { - beforeEach(() => { - getFeatureFlag.mockImplementation((key) => { - if (key === FeatureFlag.NewDeviceVerificationTemporaryDismiss) { - return Promise.resolve(true); - } - - return Promise.resolve(false); - }); - }); - - afterAll(() => { - getFeatureFlag.mockReturnValue(false); - }); - - it("redirects to notice when the user has not dismissed it", async () => { - noticeState$.mockReturnValueOnce(new BehaviorSubject(null)); - - await newDeviceGuard(); - - expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]); - expect(noticeState$).toHaveBeenCalledWith(account.id); - }); - - it("redirects to notice when the user dismissed it more than 7 days ago", async () => { - const eighteenDaysAgo = new Date(); - eighteenDaysAgo.setDate(eighteenDaysAgo.getDate() - 18); - - noticeState$.mockReturnValueOnce( - new BehaviorSubject({ last_dismissal: eighteenDaysAgo.toISOString() }), - ); - - await newDeviceGuard(); - - expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]); - }); - - it("returns true when the user dismissed less than 7 days ago", async () => { - const fourDaysAgo = new Date(); - fourDaysAgo.setDate(fourDaysAgo.getDate() - 4); - - noticeState$.mockReturnValueOnce( - new BehaviorSubject({ last_dismissal: fourDaysAgo.toISOString() }), - ); - - expect(await newDeviceGuard()).toBe(true); - }); - }); - - describe("permanent flag", () => { - beforeEach(() => { - getFeatureFlag.mockImplementation((key) => { - if (key === FeatureFlag.NewDeviceVerificationPermanentDismiss) { - return Promise.resolve(true); - } - - return Promise.resolve(false); - }); - }); - - afterAll(() => { - getFeatureFlag.mockReturnValue(false); - }); - - it("redirects when the user has not dismissed", async () => { - noticeState$.mockReturnValueOnce(new BehaviorSubject(null)); - - await newDeviceGuard(); - - expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]); - - noticeState$.mockReturnValueOnce(new BehaviorSubject({ permanent_dismissal: null })); - - await newDeviceGuard(); - - expect(createUrlTree).toHaveBeenCalledTimes(2); - expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]); - }); - - it("returns `true` when the user has dismissed", async () => { - noticeState$.mockReturnValueOnce(new BehaviorSubject({ permanent_dismissal: true })); - - expect(await newDeviceGuard()).toBe(true); - }); - }); -}); diff --git a/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts b/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts deleted file mode 100644 index 4e26149249..0000000000 --- a/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { inject } from "@angular/core"; -import { ActivatedRouteSnapshot, CanActivateFn, Router } from "@angular/router"; -import { firstValueFrom, Observable } from "rxjs"; - -import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -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 { NewDeviceVerificationNoticeService } from "@bitwarden/vault"; - -import { VaultProfileService } from "../services/vault-profile.service"; - -export const NewDeviceVerificationNoticeGuard: CanActivateFn = async ( - route: ActivatedRouteSnapshot, -) => { - const router = inject(Router); - const configService = inject(ConfigService); - const newDeviceVerificationNoticeService = inject(NewDeviceVerificationNoticeService); - const accountService = inject(AccountService); - const platformUtilsService = inject(PlatformUtilsService); - const vaultProfileService = inject(VaultProfileService); - const userVerificationService = inject(UserVerificationService); - - if (route.queryParams["fromNewDeviceVerification"]) { - return true; - } - - const tempNoticeFlag = await configService.getFeatureFlag( - FeatureFlag.NewDeviceVerificationTemporaryDismiss, - ); - const permNoticeFlag = await configService.getFeatureFlag( - FeatureFlag.NewDeviceVerificationPermanentDismiss, - ); - - if (!tempNoticeFlag && !permNoticeFlag) { - return true; - } - - const currentAcct$: Observable = accountService.activeAccount$; - const currentAcct = await firstValueFrom(currentAcct$); - - if (!currentAcct) { - return router.createUrlTree(["/login"]); - } - - // Currently used by the auth recovery login flow and will get cleaned up in PM-18485. - if (await firstValueFrom(newDeviceVerificationNoticeService.skipState$(currentAcct.id))) { - return true; - } - - try { - const isSelfHosted = platformUtilsService.isSelfHost(); - const userIsSSOUser = await ssoAppliesToUser( - userVerificationService, - vaultProfileService, - currentAcct.id, - ); - const has2FAEnabled = await hasATwoFactorProviderEnabled(vaultProfileService, currentAcct.id); - const isProfileLessThanWeekOld = await profileIsLessThanWeekOld( - vaultProfileService, - currentAcct.id, - ); - - // When any of the following are true, the device verification notice is - // not applicable for the user. When the user has *not* logged in with their - // master password, assume they logged in with SSO. - if (has2FAEnabled || isSelfHosted || userIsSSOUser || isProfileLessThanWeekOld) { - return true; - } - } catch { - // Skip showing the notice if there was a problem determining applicability - // The most likely problem to occur is the user not having a network connection - return true; - } - - const userItems$ = newDeviceVerificationNoticeService.noticeState$(currentAcct.id); - const userItems = await firstValueFrom(userItems$); - - // Show the notice when: - // - The temp notice flag is enabled - // - The user hasn't dismissed the notice or the user dismissed it more than 7 days ago - if ( - tempNoticeFlag && - (!userItems?.last_dismissal || isMoreThan7DaysAgo(userItems?.last_dismissal)) - ) { - return router.createUrlTree(["/new-device-notice"]); - } - - // Show the notice when: - // - The permanent notice flag is enabled - // - The user hasn't dismissed the notice - if (permNoticeFlag && !userItems?.permanent_dismissal) { - return router.createUrlTree(["/new-device-notice"]); - } - - return true; -}; - -/** Returns true has one 2FA provider enabled */ -async function hasATwoFactorProviderEnabled( - vaultProfileService: VaultProfileService, - userId: string, -): Promise { - return vaultProfileService.getProfileTwoFactorEnabled(userId); -} - -/** Returns true when the user's profile is less than a week old */ -async function profileIsLessThanWeekOld( - vaultProfileService: VaultProfileService, - userId: string, -): Promise { - const creationDate = await vaultProfileService.getProfileCreationDate(userId); - return !isMoreThan7DaysAgo(creationDate); -} - -/** - * Returns true when either: - * - The user is SSO bound to an organization and is not an Admin or Owner - * - The user is an Admin or Owner of an organization with SSO bound and has not logged in with their master password - * - * NOTE: There are edge cases where this does not satisfy the original requirement of showing the notice to - * users who are subject to the SSO required policy. When Owners and Admins log in with their MP they will see the notice - * when they log in with SSO they will not. This is a concession made because the original logic references policies would not work for TDE users. - * When this guard is run for those users a sync hasn't occurred and thus the policies are not available. - */ -async function ssoAppliesToUser( - userVerificationService: UserVerificationService, - vaultProfileService: VaultProfileService, - userId: string, -) { - const userSSOBound = await vaultProfileService.getUserSSOBound(userId); - const userSSOBoundAdminOwner = await vaultProfileService.getUserSSOBoundAdminOwner(userId); - const userLoggedInWithMP = await userLoggedInWithMasterPassword(userVerificationService, userId); - - const nonOwnerAdminSsoUser = userSSOBound && !userSSOBoundAdminOwner; - const ssoAdminOwnerLoggedInWithMP = userSSOBoundAdminOwner && !userLoggedInWithMP; - - return nonOwnerAdminSsoUser || ssoAdminOwnerLoggedInWithMP; -} - -/** - * Returns true when the user logged in with their master password. - */ -async function userLoggedInWithMasterPassword( - userVerificationService: UserVerificationService, - userId: string, -) { - return userVerificationService.hasMasterPasswordAndMasterKeyHash(userId); -} - -/** Returns the true when the date given is older than 7 days */ -function isMoreThan7DaysAgo(date?: string | Date): boolean { - if (!date) { - return false; - } - - const inputDate = new Date(date).getTime(); - const today = new Date().getTime(); - - const differenceInMS = today - inputDate; - const msInADay = 1000 * 60 * 60 * 24; - const differenceInDays = Math.round(differenceInMS / msInADay); - - return differenceInDays > 7; -} diff --git a/libs/angular/src/vault/services/vault-profile.service.spec.ts b/libs/angular/src/vault/services/vault-profile.service.spec.ts index ade34da39a..45e025e22d 100644 --- a/libs/angular/src/vault/services/vault-profile.service.spec.ts +++ b/libs/angular/src/vault/services/vault-profile.service.spec.ts @@ -68,94 +68,4 @@ describe("VaultProfileService", () => { expect(getProfile).not.toHaveBeenCalled(); }); }); - - describe("getProfileTwoFactorEnabled", () => { - it("calls `getProfile` when stored 2FA property is not stored", async () => { - expect(service["profile2FAEnabled"]).toBeNull(); - - const twoFactorEnabled = await service.getProfileTwoFactorEnabled(userId); - - expect(twoFactorEnabled).toBe(true); - expect(getProfile).toHaveBeenCalled(); - }); - - it("calls `getProfile` when stored profile id does not match", async () => { - service["profile2FAEnabled"] = false; - service["userId"] = "old-user-id"; - - const twoFactorEnabled = await service.getProfileTwoFactorEnabled(userId); - - expect(twoFactorEnabled).toBe(true); - expect(getProfile).toHaveBeenCalled(); - }); - - it("does not call `getProfile` when 2FA property is already stored", async () => { - service["profile2FAEnabled"] = false; - - const twoFactorEnabled = await service.getProfileTwoFactorEnabled(userId); - - expect(twoFactorEnabled).toBe(false); - expect(getProfile).not.toHaveBeenCalled(); - }); - }); - - describe("getUserSSOBound", () => { - it("calls `getProfile` when stored ssoBound property is not stored", async () => { - expect(service["userIsSsoBound"]).toBeNull(); - - const userIsSsoBound = await service.getUserSSOBound(userId); - - expect(userIsSsoBound).toBe(true); - expect(getProfile).toHaveBeenCalled(); - }); - - it("calls `getProfile` when stored profile id does not match", async () => { - service["userIsSsoBound"] = false; - service["userId"] = "old-user-id"; - - const userIsSsoBound = await service.getUserSSOBound(userId); - - expect(userIsSsoBound).toBe(true); - expect(getProfile).toHaveBeenCalled(); - }); - - it("does not call `getProfile` when ssoBound property is already stored", async () => { - service["userIsSsoBound"] = false; - - const userIsSsoBound = await service.getUserSSOBound(userId); - - expect(userIsSsoBound).toBe(false); - expect(getProfile).not.toHaveBeenCalled(); - }); - }); - - describe("getUserSSOBoundAdminOwner", () => { - it("calls `getProfile` when stored userIsSsoBoundAdminOwner property is not stored", async () => { - expect(service["userIsSsoBoundAdminOwner"]).toBeNull(); - - const userIsSsoBoundAdminOwner = await service.getUserSSOBoundAdminOwner(userId); - - expect(userIsSsoBoundAdminOwner).toBe(true); - expect(getProfile).toHaveBeenCalled(); - }); - - it("calls `getProfile` when stored profile id does not match", async () => { - service["userIsSsoBoundAdminOwner"] = false; - service["userId"] = "old-user-id"; - - const userIsSsoBoundAdminOwner = await service.getUserSSOBoundAdminOwner(userId); - - expect(userIsSsoBoundAdminOwner).toBe(true); - expect(getProfile).toHaveBeenCalled(); - }); - - it("does not call `getProfile` when userIsSsoBoundAdminOwner property is already stored", async () => { - service["userIsSsoBoundAdminOwner"] = false; - - const userIsSsoBoundAdminOwner = await service.getUserSSOBoundAdminOwner(userId); - - expect(userIsSsoBoundAdminOwner).toBe(false); - expect(getProfile).not.toHaveBeenCalled(); - }); - }); }); diff --git a/libs/angular/src/vault/services/vault-profile.service.ts b/libs/angular/src/vault/services/vault-profile.service.ts index 21f4ecc228..3a8c9d4ee9 100644 --- a/libs/angular/src/vault/services/vault-profile.service.ts +++ b/libs/angular/src/vault/services/vault-profile.service.ts @@ -1,18 +1,13 @@ import { Injectable, inject } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { ProfileResponse } from "@bitwarden/common/models/response/profile.response"; @Injectable({ providedIn: "root", }) /** - * Class to provide profile level details without having to call the API each time. - * NOTE: This is a temporary service and can be replaced once the `UnauthenticatedExtensionUIRefresh` flag goes live. - * The `UnauthenticatedExtensionUIRefresh` introduces a sync that takes place upon logging in. These details can then - * be added to account object and retrieved from there. - * TODO: PM-16202 + * Class to provide profile level details to vault entities without having to call the API each time. */ export class VaultProfileService { private apiService = inject(ApiService); @@ -22,15 +17,6 @@ export class VaultProfileService { /** Profile creation stored as a string. */ private profileCreatedDate: string | null = null; - /** True when 2FA is enabled on the profile. */ - private profile2FAEnabled: boolean | null = null; - - /** True when ssoBound is true for any of the users organizations */ - private userIsSsoBound: boolean | null = null; - - /** True when the user is an admin or owner of the ssoBound organization */ - private userIsSsoBoundAdminOwner: boolean | null = null; - /** * Returns the creation date of the profile. * Note: `Date`s are mutable in JS, creating a new @@ -46,56 +32,11 @@ export class VaultProfileService { return new Date(profile.creationDate); } - /** - * Returns whether there is a 2FA provider on the profile. - */ - async getProfileTwoFactorEnabled(userId: string): Promise { - if (this.profile2FAEnabled !== null && userId === this.userId) { - return Promise.resolve(this.profile2FAEnabled); - } - - const profile = await this.fetchAndCacheProfile(); - - return profile.twoFactorEnabled; - } - - /** - * Returns whether the user logs in with SSO for any organization. - */ - async getUserSSOBound(userId: string): Promise { - if (this.userIsSsoBound !== null && userId === this.userId) { - return Promise.resolve(this.userIsSsoBound); - } - - await this.fetchAndCacheProfile(); - - return !!this.userIsSsoBound; - } - - /** - * Returns true when the user is an Admin or Owner of an organization with `ssoBound` true. - */ - async getUserSSOBoundAdminOwner(userId: string): Promise { - if (this.userIsSsoBoundAdminOwner !== null && userId === this.userId) { - return Promise.resolve(this.userIsSsoBoundAdminOwner); - } - - await this.fetchAndCacheProfile(); - - return !!this.userIsSsoBoundAdminOwner; - } - private async fetchAndCacheProfile(): Promise { const profile = await this.apiService.getProfile(); this.userId = profile.id; this.profileCreatedDate = profile.creationDate; - this.profile2FAEnabled = profile.twoFactorEnabled; - const ssoBoundOrg = profile.organizations.find((org) => org.ssoBound); - this.userIsSsoBound = !!ssoBoundOrg; - this.userIsSsoBoundAdminOwner = - ssoBoundOrg?.type === OrganizationUserType.Admin || - ssoBoundOrg?.type === OrganizationUserType.Owner; return profile; } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 7174923f12..930b6a3b91 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -55,8 +55,7 @@ export enum FeatureFlag { /* Vault */ PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge", PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form", - NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss", - NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss", + VaultBulkManagementAction = "vault-bulk-management-action", SecurityTasks = "security-tasks", CipherKeyEncryption = "cipher-key-encryption", PM18520_UpdateDesktopCipherForm = "pm-18520-desktop-cipher-forms", @@ -108,8 +107,7 @@ export const DefaultFeatureFlagValue = { /* Vault */ [FeatureFlag.PM8851_BrowserOnboardingNudge]: FALSE, [FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE, - [FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE, - [FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE, + [FeatureFlag.VaultBulkManagementAction]: FALSE, [FeatureFlag.SecurityTasks]: FALSE, [FeatureFlag.CipherKeyEncryption]: FALSE, [FeatureFlag.PM18520_UpdateDesktopCipherForm]: FALSE, diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 587212299d..d93a3f4da7 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -195,13 +195,6 @@ export const PREMIUM_BANNER_DISK_LOCAL = new StateDefinition("premiumBannerRepro web: "disk-local", }); export const BANNERS_DISMISSED_DISK = new StateDefinition("bannersDismissed", "disk"); -export const NEW_DEVICE_VERIFICATION_NOTICE = new StateDefinition( - "newDeviceVerificationNotice", - "disk", - { - web: "disk-local", - }, -); export const VAULT_APPEARANCE = new StateDefinition("vaultAppearance", "disk"); export const SECURITY_TASKS_DISK = new StateDefinition("securityTasks", "disk"); export const AT_RISK_PASSWORDS_PAGE_DISK = new StateDefinition("atRiskPasswordsPage", "disk"); diff --git a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.html b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.html deleted file mode 100644 index 812a8faa66..0000000000 --- a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.html +++ /dev/null @@ -1,33 +0,0 @@ -
-

- {{ "newDeviceVerificationNoticeContentPage1" | i18n }} - - {{ "learnMore" | i18n }}. - -

- - -

- {{ "newDeviceVerificationNoticePageOneFormContent" | i18n: this.currentEmail }} -

- - - - {{ "newDeviceVerificationNoticePageOneEmailAccessNo" | i18n }} - - - {{ "newDeviceVerificationNoticePageOneEmailAccessYes" | i18n }} - - -
- - -
diff --git a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.spec.ts b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.spec.ts deleted file mode 100644 index c6eccb7873..0000000000 --- a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.spec.ts +++ /dev/null @@ -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; - - 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, - }); - }); - }); - }); -}); diff --git a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.ts b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.ts deleted file mode 100644 index 8db923fec8..0000000000 --- a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.ts +++ /dev/null @@ -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 = 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/"); - } -} diff --git a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.html b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.html deleted file mode 100644 index 1129e4750d..0000000000 --- a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.html +++ /dev/null @@ -1,48 +0,0 @@ -

- {{ "newDeviceVerificationNoticeContentPage2" | i18n }} -

- - - {{ "turnOnTwoStepLogin" | i18n }} - - - - {{ "changeAcctEmail" | i18n }} - - - - diff --git a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.spec.ts b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.spec.ts deleted file mode 100644 index 92f0494776..0000000000 --- a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.spec.ts +++ /dev/null @@ -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; - - 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(); - }); - }); -}); diff --git a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.ts b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.ts deleted file mode 100644 index a2d958d53f..0000000000 --- a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.ts +++ /dev/null @@ -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 = this.accountService.activeAccount$; - private currentUserId: UserId | null = null; - private env$: Observable = 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"]); - } -} diff --git a/libs/vault/src/icons/exclamation-triangle.ts b/libs/vault/src/icons/exclamation-triangle.ts deleted file mode 100644 index 6340546d1e..0000000000 --- a/libs/vault/src/icons/exclamation-triangle.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { svgIcon } from "@bitwarden/components"; - -export const ExclamationTriangle = svgIcon` - - - -`; diff --git a/libs/vault/src/icons/index.ts b/libs/vault/src/icons/index.ts index 7a412cf050..ef4a7f52a3 100644 --- a/libs/vault/src/icons/index.ts +++ b/libs/vault/src/icons/index.ts @@ -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"; diff --git a/libs/vault/src/icons/user-lock.ts b/libs/vault/src/icons/user-lock.ts deleted file mode 100644 index c1dc3efde3..0000000000 --- a/libs/vault/src/icons/user-lock.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { svgIcon } from "@bitwarden/components"; - -export const UserLock = svgIcon` - - - - - - - - - - - - - -`; diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index 87e15b1867..bdbd5aa36c 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -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"; diff --git a/libs/vault/src/services/new-device-verification-notice.service.spec.ts b/libs/vault/src/services/new-device-verification-notice.service.spec.ts deleted file mode 100644 index 186a844c16..0000000000 --- a/libs/vault/src/services/new-device-verification-notice.service.spec.ts +++ /dev/null @@ -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; - let mockSkipState: FakeSingleUserState; - 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); - }); - }); -}); diff --git a/libs/vault/src/services/new-device-verification-notice.service.ts b/libs/vault/src/services/new-device-verification-notice.service.ts deleted file mode 100644 index a0d77f09f5..0000000000 --- a/libs/vault/src/services/new-device-verification-notice.service.ts +++ /dev/null @@ -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) { - if (obj == null) { - return; - } - this.last_dismissal = obj.last_dismissal || null; - this.permanent_dismissal = obj.permanent_dismissal || null; - } - - static fromJSON(obj: Jsonify) { - return Object.assign(new NewDeviceVerificationNotice({}), obj); - } -} - -export const NEW_DEVICE_VERIFICATION_NOTICE_KEY = - new UserKeyDefinition( - NEW_DEVICE_VERIFICATION_NOTICE, - "noticeState", - { - deserializer: (obj: Jsonify) => - NewDeviceVerificationNotice.fromJSON(obj), - clearOn: [], - }, - ); - -export const SKIP_NEW_DEVICE_VERIFICATION_NOTICE = new UserKeyDefinition( - NEW_DEVICE_VERIFICATION_NOTICE, - "shouldSkip", - { - deserializer: (data: boolean) => data, - clearOn: ["logout"], - }, -); - -@Injectable() -export class NewDeviceVerificationNoticeService { - constructor(private stateProvider: StateProvider) {} - - private noticeState(userId: UserId): SingleUserState { - return this.stateProvider.getUser(userId, NEW_DEVICE_VERIFICATION_NOTICE_KEY); - } - - noticeState$(userId: UserId): Observable { - return this.noticeState(userId).state$; - } - - async updateNewDeviceVerificationNoticeState( - userId: UserId, - newState: NewDeviceVerificationNotice, - ): Promise { - await this.noticeState(userId).update(() => { - return { ...newState }; - }); - } - - private skipState(userId: UserId): SingleUserState { - return this.stateProvider.getUser(userId, SKIP_NEW_DEVICE_VERIFICATION_NOTICE); - } - - skipState$(userId: UserId): Observable { - return this.skipState(userId).state$; - } - - async updateNewDeviceVerificationSkipNoticeState( - userId: UserId, - shouldSkip: boolean, - ): Promise { - await this.skipState(userId).update(() => shouldSkip); - } -}