1
0
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:
Conner Turnbull
2024-03-15 15:53:05 -04:00
committed by GitHub
parent 65534a1323
commit b99153a016
85 changed files with 942 additions and 261 deletions

View File

@@ -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>;
}

View File

@@ -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);
});
});
});

View File

@@ -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,
};
});
}
}