diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 51417c16fb..452624d77e 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -576,7 +576,7 @@ export default class MainBackground { ); this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( - this.activeUserStateProvider, + this.stateProvider, ); this.loginStrategyService = new LoginStrategyService( diff --git a/apps/browser/src/platform/background/service-factories/billing-account-profile-state-service.factory.ts b/apps/browser/src/platform/background/service-factories/billing-account-profile-state-service.factory.ts index 80482eacb6..378707d6be 100644 --- a/apps/browser/src/platform/background/service-factories/billing-account-profile-state-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/billing-account-profile-state-service.factory.ts @@ -1,9 +1,8 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; -import { activeUserStateProviderFactory } from "./active-user-state-provider.factory"; import { FactoryOptions, CachedServices, factory } from "./factory-options"; -import { StateProviderInitOptions } from "./state-provider.factory"; +import { StateProviderInitOptions, stateProviderFactory } from "./state-provider.factory"; type BillingAccountProfileStateServiceFactoryOptions = FactoryOptions; @@ -21,8 +20,6 @@ export function billingAccountProfileStateServiceFactory( "billingAccountProfileStateService", opts, async () => - new DefaultBillingAccountProfileStateService( - await activeUserStateProviderFactory(cache, opts), - ), + new DefaultBillingAccountProfileStateService(await stateProviderFactory(cache, opts)), ); } diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 5c6423708a..360ac6ffc4 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -467,7 +467,7 @@ export class Main { ); this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( - this.activeUserStateProvider, + this.stateProvider, ); this.loginStrategyService = new LoginStrategyService( diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 57a3fe63ab..cab71631da 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1067,7 +1067,7 @@ const typesafeProviders: Array = [ safeProvider({ provide: BillingAccountProfileStateService, useClass: DefaultBillingAccountProfileStateService, - deps: [ActiveUserStateProvider], + deps: [StateProvider], }), safeProvider({ provide: OrganizationManagementPreferencesService, diff --git a/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts b/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts index 4a2a94e9c6..7f0f218a23 100644 --- a/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts +++ b/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts @@ -2,10 +2,10 @@ import { firstValueFrom } from "rxjs"; import { FakeAccountService, - FakeActiveUserStateProvider, mockAccountServiceWith, FakeActiveUserState, - trackEmissions, + FakeStateProvider, + FakeSingleUserState, } from "../../../../spec"; import { UserId } from "../../../types/guid"; import { BillingAccountProfile } from "../../abstractions/account/billing-account-profile-state.service"; @@ -16,20 +16,26 @@ import { } from "./billing-account-profile-state.service"; describe("BillingAccountProfileStateService", () => { - let activeUserStateProvider: FakeActiveUserStateProvider; + let stateProvider: FakeStateProvider; let sut: DefaultBillingAccountProfileStateService; let billingAccountProfileState: FakeActiveUserState; + let userBillingAccountProfileState: FakeSingleUserState; let accountService: FakeAccountService; const userId = "fakeUserId" as UserId; beforeEach(() => { accountService = mockAccountServiceWith(userId); - activeUserStateProvider = new FakeActiveUserStateProvider(accountService); + stateProvider = new FakeStateProvider(accountService); - sut = new DefaultBillingAccountProfileStateService(activeUserStateProvider); + sut = new DefaultBillingAccountProfileStateService(stateProvider); - billingAccountProfileState = activeUserStateProvider.getFake( + billingAccountProfileState = stateProvider.activeUser.getFake( + BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, + ); + + userBillingAccountProfileState = stateProvider.singleUser.getFake( + userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, ); }); @@ -38,9 +44,9 @@ describe("BillingAccountProfileStateService", () => { return jest.resetAllMocks(); }); - describe("accountHasPremiumFromAnyOrganization$", () => { - it("should emit changes in hasPremiumFromAnyOrganization", async () => { - billingAccountProfileState.nextState({ + describe("hasPremiumFromAnyOrganization$", () => { + it("returns true when they have premium from an organization", async () => { + userBillingAccountProfileState.nextState({ hasPremiumPersonally: false, hasPremiumFromAnyOrganization: true, }); @@ -48,118 +54,91 @@ describe("BillingAccountProfileStateService", () => { expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(true); }); - it("should emit once when calling setHasPremium once", async () => { - const emissions = trackEmissions(sut.hasPremiumFromAnyOrganization$); - const startingEmissionCount = emissions.length; - - await sut.setHasPremium(true, true); - - const endingEmissionCount = emissions.length; - expect(endingEmissionCount - startingEmissionCount).toBe(1); - }); - }); - - describe("hasPremiumPersonally$", () => { - it("should emit changes in hasPremiumPersonally", async () => { - billingAccountProfileState.nextState({ - hasPremiumPersonally: true, - hasPremiumFromAnyOrganization: false, - }); - - expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true); - }); - - it("should emit once when calling setHasPremium once", async () => { - const emissions = trackEmissions(sut.hasPremiumPersonally$); - const startingEmissionCount = emissions.length; - - await sut.setHasPremium(true, true); - - const endingEmissionCount = emissions.length; - expect(endingEmissionCount - startingEmissionCount).toBe(1); - }); - }); - - describe("canAccessPremium$", () => { - it("should emit changes in hasPremiumPersonally", async () => { - billingAccountProfileState.nextState({ - hasPremiumPersonally: true, - hasPremiumFromAnyOrganization: false, - }); - - expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); - }); - - it("should emit changes in hasPremiumFromAnyOrganization", async () => { - billingAccountProfileState.nextState({ + it("return false when they do not have premium from an organization", async () => { + userBillingAccountProfileState.nextState({ hasPremiumPersonally: false, - hasPremiumFromAnyOrganization: true, + hasPremiumFromAnyOrganization: false, }); - expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); - }); - - it("should emit changes in both hasPremiumPersonally and hasPremiumFromAnyOrganization", async () => { - billingAccountProfileState.nextState({ - hasPremiumPersonally: true, - hasPremiumFromAnyOrganization: true, - }); - - expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); - }); - - it("should emit once when calling setHasPremium once", async () => { - const emissions = trackEmissions(sut.hasPremiumFromAnySource$); - const startingEmissionCount = emissions.length; - - await sut.setHasPremium(true, true); - - const endingEmissionCount = emissions.length; - expect(endingEmissionCount - startingEmissionCount).toBe(1); - }); - }); - - describe("setHasPremium", () => { - it("should have `hasPremiumPersonally$` emit `true` when passing `true` as an argument for hasPremiumPersonally", async () => { - await sut.setHasPremium(true, false); - - expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true); - }); - - it("should have `hasPremiumFromAnyOrganization$` emit `true` when passing `true` as an argument for hasPremiumFromAnyOrganization", async () => { - await sut.setHasPremium(false, true); - - expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(true); - }); - - it("should have `hasPremiumPersonally$` emit `false` when passing `false` as an argument for hasPremiumPersonally", async () => { - await sut.setHasPremium(false, false); - - expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(false); - }); - - it("should have `hasPremiumFromAnyOrganization$` emit `false` when passing `false` as an argument for hasPremiumFromAnyOrganization", async () => { - await sut.setHasPremium(false, false); - expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false); }); - it("should have `canAccessPremium$` emit `true` when passing `true` as an argument for hasPremiumPersonally", async () => { - await sut.setHasPremium(true, false); + it("returns false when there is no active user", async () => { + await accountService.switchAccount(null); + + expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false); + }); + }); + + describe("hasPremiumPersonally$", () => { + it("returns true when the user has premium personally", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: true, + hasPremiumFromAnyOrganization: false, + }); + + expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true); + }); + + it("returns false when the user does not have premium personally", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: false, + hasPremiumFromAnyOrganization: false, + }); + + expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(false); + }); + + it("returns false when there is no active user", async () => { + await accountService.switchAccount(null); + + expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(false); + }); + }); + + describe("hasPremiumFromAnySource$", () => { + it("returns true when the user has premium personally", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: true, + hasPremiumFromAnyOrganization: false, + }); expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); }); - it("should have `canAccessPremium$` emit `true` when passing `true` as an argument for hasPremiumFromAnyOrganization", async () => { - await sut.setHasPremium(false, true); + it("returns true when the user has premium from an organization", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: false, + hasPremiumFromAnyOrganization: true, + }); expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); }); - it("should have `canAccessPremium$` emit `false` when passing `false` for all arguments", async () => { - await sut.setHasPremium(false, false); + it("returns true when they have premium personally AND from an organization", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: true, + hasPremiumFromAnyOrganization: true, + }); + + expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); + }); + + it("returns false when there is no active user", async () => { + await accountService.switchAccount(null); expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(false); }); }); + + describe("setHasPremium", () => { + it("should update the active users state when called", async () => { + await sut.setHasPremium(true, false); + + expect(billingAccountProfileState.nextMock).toHaveBeenCalledWith([ + userId, + { hasPremiumPersonally: true, hasPremiumFromAnyOrganization: false }, + ]); + }); + }); }); diff --git a/libs/common/src/billing/services/account/billing-account-profile-state.service.ts b/libs/common/src/billing/services/account/billing-account-profile-state.service.ts index c6b6f104a8..336021c993 100644 --- a/libs/common/src/billing/services/account/billing-account-profile-state.service.ts +++ b/libs/common/src/billing/services/account/billing-account-profile-state.service.ts @@ -1,10 +1,10 @@ -import { map, Observable } from "rxjs"; +import { map, Observable, of, switchMap } from "rxjs"; import { ActiveUserState, - ActiveUserStateProvider, BILLING_DISK, KeyDefinition, + StateProvider, } from "../../../platform/state"; import { BillingAccountProfile, @@ -26,24 +26,34 @@ export class DefaultBillingAccountProfileStateService implements BillingAccountP hasPremiumPersonally$: Observable; hasPremiumFromAnySource$: Observable; - constructor(activeUserStateProvider: ActiveUserStateProvider) { - this.billingAccountProfileState = activeUserStateProvider.get( + constructor(stateProvider: StateProvider) { + this.billingAccountProfileState = stateProvider.getActive( BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, ); - this.hasPremiumFromAnyOrganization$ = this.billingAccountProfileState.state$.pipe( + // Setup an observable that will always track the currently active user + // but will fallback to emitting null when there is no active user. + const billingAccountProfileOrNull = stateProvider.activeUserId$.pipe( + switchMap((userId) => + userId != null + ? stateProvider.getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION).state$ + : of(null), + ), + ); + + this.hasPremiumFromAnyOrganization$ = billingAccountProfileOrNull.pipe( map((billingAccountProfile) => !!billingAccountProfile?.hasPremiumFromAnyOrganization), ); - this.hasPremiumPersonally$ = this.billingAccountProfileState.state$.pipe( + this.hasPremiumPersonally$ = billingAccountProfileOrNull.pipe( map((billingAccountProfile) => !!billingAccountProfile?.hasPremiumPersonally), ); - this.hasPremiumFromAnySource$ = this.billingAccountProfileState.state$.pipe( + this.hasPremiumFromAnySource$ = billingAccountProfileOrNull.pipe( map( (billingAccountProfile) => - billingAccountProfile?.hasPremiumFromAnyOrganization || - billingAccountProfile?.hasPremiumPersonally, + billingAccountProfile?.hasPremiumFromAnyOrganization === true || + billingAccountProfile?.hasPremiumPersonally === true, ), ); }