From 21855595c56f72a779906d7796060f4782409d6a Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Tue, 19 Nov 2024 10:04:40 -0500 Subject: [PATCH] [PM-14219] Add service for new device verification notice (#11988) * added service and spec file for new device verification notice --- .../src/platform/state/state-definitions.ts | 4 + ...device-verification-notice.service.spec.ts | 85 +++++++++++++++++++ .../new-device-verification-notice.service.ts | 65 ++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 libs/vault/src/services/new-device-verification-notice.service.spec.ts create mode 100644 libs/vault/src/services/new-device-verification-notice.service.ts diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 47b7199b94..0779d80982 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -173,3 +173,7 @@ export const PREMIUM_BANNER_DISK_LOCAL = new StateDefinition("premiumBannerRepro }); export const BANNERS_DISMISSED_DISK = new StateDefinition("bannersDismissed", "disk"); export const VAULT_BROWSER_UI_ONBOARDING = new StateDefinition("vaultBrowserUiOnboarding", "disk"); +export const NEW_DEVICE_VERIFICATION_NOTICE = new StateDefinition( + "newDeviceVerificationNotice", + "disk", +); 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 new file mode 100644 index 0000000000..b774b1a05a --- /dev/null +++ b/libs/vault/src/services/new-device-verification-notice.service.spec.ts @@ -0,0 +1,85 @@ +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, +} 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 stateProvider: FakeStateProvider; + let accountService: FakeAccountService; + + beforeEach(() => { + accountService = mockAccountServiceWith(userId); + stateProvider = new FakeStateProvider(accountService); + mockNoticeState = stateProvider.singleUser.getFake(userId, NEW_DEVICE_VERIFICATION_NOTICE_KEY); + 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); + }); + }); +}); diff --git a/libs/vault/src/services/new-device-verification-notice.service.ts b/libs/vault/src/services/new-device-verification-notice.service.ts new file mode 100644 index 0000000000..949fa59823 --- /dev/null +++ b/libs/vault/src/services/new-device-verification-notice.service.ts @@ -0,0 +1,65 @@ +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; + permanent_dismissal: boolean; + + 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: [], + }, + ); + +@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 }; + }); + } +}