mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 16:53:34 +00:00
[AC-2156] Billing State Provider Migration (#8133)
* Added billing account profile state service
* Update usages after removing state service functions
* Added migrator
* Updated bw.ts and main.background.ts
* Removed comment
* Updated state service dependencies to include billing service
* Added missing mv3 factory and updated MainContextMenuHandler
* updated autofill service and tests
* Updated the remaining extensions usages
* Updated desktop
* Removed subjects where they weren't needed
* Refactored billing service to have a single setter to avoid unecessary emissions
* Refactored has premium guard to return an observable
* Renamed services to match ADR
f633f2cdd8/docs/architecture/clients/presentation/angular.md (abstract--default-implementations)
* Updated property names to be a smidgen more descriptive and added jsdocs
* Updated setting of canAccessPremium to automatically update when the underlying observable emits
* Fixed build error after merge conflicts
* Another build error from conflict
* Removed autofill unit test changes from conflict
* Updated login strategy to not set premium field using state service
* Updated CLI to use billing state provider
* Shortened names a bit
* Fixed build
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
export type BillingAccountProfile = {
|
||||
hasPremiumPersonally: boolean;
|
||||
hasPremiumFromAnyOrganization: boolean;
|
||||
};
|
||||
|
||||
export abstract class BillingAccountProfileStateService {
|
||||
/**
|
||||
* Emits `true` when the active user's account has been granted premium from any of the
|
||||
* organizations it is a member of. Otherwise, emits `false`
|
||||
*/
|
||||
hasPremiumFromAnyOrganization$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Emits `true` when the active user's account has an active premium subscription at the
|
||||
* individual user level
|
||||
*/
|
||||
hasPremiumPersonally$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Emits `true` when either `hasPremiumPersonally` or `hasPremiumFromAnyOrganization` is `true`
|
||||
*/
|
||||
hasPremiumFromAnySource$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Sets the active user's premium status fields upon every full sync, either from their personal
|
||||
* subscription to premium, or an organization they're a part of that grants them premium.
|
||||
* @param hasPremiumPersonally
|
||||
* @param hasPremiumFromAnyOrganization
|
||||
*/
|
||||
abstract setHasPremium(
|
||||
hasPremiumPersonally: boolean,
|
||||
hasPremiumFromAnyOrganization: boolean,
|
||||
): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import {
|
||||
FakeAccountService,
|
||||
FakeActiveUserStateProvider,
|
||||
mockAccountServiceWith,
|
||||
FakeActiveUserState,
|
||||
trackEmissions,
|
||||
} from "../../../../spec";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { BillingAccountProfile } from "../../abstractions/account/billing-account-profile-state.service";
|
||||
|
||||
import {
|
||||
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
|
||||
DefaultBillingAccountProfileStateService,
|
||||
} from "./billing-account-profile-state.service";
|
||||
|
||||
describe("BillingAccountProfileStateService", () => {
|
||||
let activeUserStateProvider: FakeActiveUserStateProvider;
|
||||
let sut: DefaultBillingAccountProfileStateService;
|
||||
let billingAccountProfileState: FakeActiveUserState<BillingAccountProfile>;
|
||||
let accountService: FakeAccountService;
|
||||
|
||||
const userId = "fakeUserId" as UserId;
|
||||
|
||||
beforeEach(() => {
|
||||
accountService = mockAccountServiceWith(userId);
|
||||
activeUserStateProvider = new FakeActiveUserStateProvider(accountService);
|
||||
|
||||
sut = new DefaultBillingAccountProfileStateService(activeUserStateProvider);
|
||||
|
||||
billingAccountProfileState = activeUserStateProvider.getFake(
|
||||
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("accountHasPremiumFromAnyOrganization$", () => {
|
||||
it("should emit changes in hasPremiumFromAnyOrganization", async () => {
|
||||
billingAccountProfileState.nextState({
|
||||
hasPremiumPersonally: false,
|
||||
hasPremiumFromAnyOrganization: true,
|
||||
});
|
||||
|
||||
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({
|
||||
hasPremiumPersonally: false,
|
||||
hasPremiumFromAnyOrganization: true,
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
ActiveUserState,
|
||||
ActiveUserStateProvider,
|
||||
BILLING_DISK,
|
||||
KeyDefinition,
|
||||
} from "../../../platform/state";
|
||||
import {
|
||||
BillingAccountProfile,
|
||||
BillingAccountProfileStateService,
|
||||
} from "../../abstractions/account/billing-account-profile-state.service";
|
||||
|
||||
export const BILLING_ACCOUNT_PROFILE_KEY_DEFINITION = new KeyDefinition<BillingAccountProfile>(
|
||||
BILLING_DISK,
|
||||
"accountProfile",
|
||||
{
|
||||
deserializer: (billingAccountProfile) => billingAccountProfile,
|
||||
},
|
||||
);
|
||||
|
||||
export class DefaultBillingAccountProfileStateService implements BillingAccountProfileStateService {
|
||||
private billingAccountProfileState: ActiveUserState<BillingAccountProfile>;
|
||||
|
||||
hasPremiumFromAnyOrganization$: Observable<boolean>;
|
||||
hasPremiumPersonally$: Observable<boolean>;
|
||||
hasPremiumFromAnySource$: Observable<boolean>;
|
||||
|
||||
constructor(activeUserStateProvider: ActiveUserStateProvider) {
|
||||
this.billingAccountProfileState = activeUserStateProvider.get(
|
||||
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
|
||||
);
|
||||
|
||||
this.hasPremiumFromAnyOrganization$ = this.billingAccountProfileState.state$.pipe(
|
||||
map((billingAccountProfile) => !!billingAccountProfile?.hasPremiumFromAnyOrganization),
|
||||
);
|
||||
|
||||
this.hasPremiumPersonally$ = this.billingAccountProfileState.state$.pipe(
|
||||
map((billingAccountProfile) => !!billingAccountProfile?.hasPremiumPersonally),
|
||||
);
|
||||
|
||||
this.hasPremiumFromAnySource$ = this.billingAccountProfileState.state$.pipe(
|
||||
map(
|
||||
(billingAccountProfile) =>
|
||||
billingAccountProfile?.hasPremiumFromAnyOrganization ||
|
||||
billingAccountProfile?.hasPremiumPersonally,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async setHasPremium(
|
||||
hasPremiumPersonally: boolean,
|
||||
hasPremiumFromAnyOrganization: boolean,
|
||||
): Promise<void> {
|
||||
await this.billingAccountProfileState.update((billingAccountProfile) => {
|
||||
return {
|
||||
hasPremiumPersonally: hasPremiumPersonally,
|
||||
hasPremiumFromAnyOrganization: hasPremiumFromAnyOrganization,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user