diff --git a/libs/common/src/auth/abstractions/account.service.ts b/libs/common/src/auth/abstractions/account.service.ts index 8b0280feb01..a48cc8d1405 100644 --- a/libs/common/src/auth/abstractions/account.service.ts +++ b/libs/common/src/auth/abstractions/account.service.ts @@ -2,14 +2,11 @@ import { Observable } from "rxjs"; import { UserId } from "../../types/guid"; -/** - * Holds information about an account for use in the AccountService - * if more information is added, be sure to update the equality method. - */ export type AccountInfo = { email: string; emailVerified: boolean; name: string | undefined; + creationDate: string | undefined; }; export type Account = { id: UserId } & AccountInfo; @@ -75,6 +72,12 @@ export abstract class AccountService { * @param emailVerified */ abstract setAccountEmailVerified(userId: UserId, emailVerified: boolean): Promise; + /** + * updates the `accounts$` observable with the creation date for the account. + * @param userId + * @param creationDate + */ + abstract setCreationDate(userId: UserId, creationDate: string): Promise; /** * updates the `accounts$` observable with the new VerifyNewDeviceLogin property for the account. * @param userId diff --git a/libs/common/src/auth/services/account.service.spec.ts b/libs/common/src/auth/services/account.service.spec.ts index 3e3c878eaac..22d66045fc0 100644 --- a/libs/common/src/auth/services/account.service.spec.ts +++ b/libs/common/src/auth/services/account.service.spec.ts @@ -27,7 +27,12 @@ import { } from "./account.service"; describe("accountInfoEqual", () => { - const accountInfo: AccountInfo = { name: "name", email: "email", emailVerified: true }; + const accountInfo: AccountInfo = { + name: "name", + email: "email", + emailVerified: true, + creationDate: "2024-01-01T00:00:00.000Z", + }; it("compares nulls", () => { expect(accountInfoEqual(null, null)).toBe(true); @@ -64,6 +69,28 @@ describe("accountInfoEqual", () => { expect(accountInfoEqual(accountInfo, same)).toBe(true); expect(accountInfoEqual(accountInfo, different)).toBe(false); }); + + it("compares creationDate", () => { + const same = { ...accountInfo }; + const different = { ...accountInfo, creationDate: "2024-12-31T00:00:00.000Z" }; + + expect(accountInfoEqual(accountInfo, same)).toBe(true); + expect(accountInfoEqual(accountInfo, different)).toBe(false); + }); + + it("compares undefined creationDate", () => { + const accountWithoutCreationDate: AccountInfo = { + name: "name", + email: "email", + emailVerified: true, + creationDate: undefined, + }; + const same = { ...accountWithoutCreationDate }; + const different = { ...accountWithoutCreationDate, creationDate: "2024-01-01T00:00:00.000Z" }; + + expect(accountInfoEqual(accountWithoutCreationDate, same)).toBe(true); + expect(accountInfoEqual(accountWithoutCreationDate, different)).toBe(false); + }); }); describe("accountService", () => { @@ -76,7 +103,12 @@ describe("accountService", () => { let activeAccountIdState: FakeGlobalState; let accountActivityState: FakeGlobalState>; const userId = Utils.newGuid() as UserId; - const userInfo = { email: "email", name: "name", emailVerified: true }; + const userInfo = { + email: "email", + name: "name", + emailVerified: true, + creationDate: "2024-01-01T00:00:00.000Z", + }; beforeEach(() => { messagingService = mock(); @@ -253,6 +285,56 @@ describe("accountService", () => { }); }); + describe("setCreationDate", () => { + const initialState = { [userId]: userInfo }; + beforeEach(() => { + accountsState.stateSubject.next(initialState); + }); + + it("should update the account with a new creation date", async () => { + const newCreationDate = "2024-12-31T00:00:00.000Z"; + await sut.setCreationDate(userId, newCreationDate); + const currentState = await firstValueFrom(accountsState.state$); + + expect(currentState).toEqual({ + [userId]: { ...userInfo, creationDate: newCreationDate }, + }); + }); + + it("should not update if the creation date is the same", async () => { + await sut.setCreationDate(userId, userInfo.creationDate); + const currentState = await firstValueFrom(accountsState.state$); + + expect(currentState).toEqual(initialState); + }); + + it("should update from undefined to a defined creation date", async () => { + const accountWithoutCreationDate: AccountInfo = { + ...userInfo, + creationDate: undefined, + }; + accountsState.stateSubject.next({ [userId]: accountWithoutCreationDate }); + + const newCreationDate = "2024-06-15T12:30:00.000Z"; + await sut.setCreationDate(userId, newCreationDate); + const currentState = await firstValueFrom(accountsState.state$); + + expect(currentState).toEqual({ + [userId]: { ...accountWithoutCreationDate, creationDate: newCreationDate }, + }); + }); + + it("should update to a different creation date string format", async () => { + const newCreationDate = "2023-03-15T08:45:30.123Z"; + await sut.setCreationDate(userId, newCreationDate); + const currentState = await firstValueFrom(accountsState.state$); + + expect(currentState).toEqual({ + [userId]: { ...userInfo, creationDate: newCreationDate }, + }); + }); + }); + describe("setAccountVerifyNewDeviceLogin", () => { const initialState = true; beforeEach(() => { @@ -294,6 +376,7 @@ describe("accountService", () => { email: "", emailVerified: false, name: undefined, + creationDate: undefined, }, }); }); diff --git a/libs/common/src/auth/services/account.service.ts b/libs/common/src/auth/services/account.service.ts index fb4b590ce77..071c174e1f2 100644 --- a/libs/common/src/auth/services/account.service.ts +++ b/libs/common/src/auth/services/account.service.ts @@ -62,6 +62,7 @@ const LOGGED_OUT_INFO: AccountInfo = { email: "", emailVerified: false, name: undefined, + creationDate: undefined, }; /** @@ -167,6 +168,10 @@ export class AccountServiceImplementation implements InternalAccountService { await this.setAccountInfo(userId, { emailVerified }); } + async setCreationDate(userId: UserId, creationDate: string): Promise { + await this.setAccountInfo(userId, { creationDate }); + } + async clean(userId: UserId) { await this.setAccountInfo(userId, LOGGED_OUT_INFO); await this.removeAccountActivity(userId); diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index 910702bddd0..04f2950c8ae 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -272,6 +272,7 @@ export class DefaultSyncService extends CoreSyncService { await this.tokenService.setSecurityStamp(response.securityStamp, response.id); await this.accountService.setAccountEmailVerified(response.id, response.emailVerified); await this.accountService.setAccountVerifyNewDeviceLogin(response.id, response.verifyDevices); + await this.accountService.setCreationDate(response.id, response.creationDate); await this.billingAccountProfileStateService.setHasPremium( response.premiumPersonally, diff --git a/libs/state/src/core/user-state.provider.ts b/libs/state/src/core/user-state.provider.ts index 82a2f873613..caadb008c56 100644 --- a/libs/state/src/core/user-state.provider.ts +++ b/libs/state/src/core/user-state.provider.ts @@ -16,8 +16,9 @@ export abstract class SingleUserStateProvider { abstract get(userId: UserId, userKeyDefinition: UserKeyDefinition): SingleUserState; } -/** A provider for getting an implementation of state scoped to a given key, but always pointing - * to the currently active user +/** + * @deprecated ActiveUserStateProvider is deprecated. Use SingleUserStateProvider instead. + * See [state README](../../README.md#should-i-use-activeuserstate) for details. */ export abstract class ActiveUserStateProvider { /**