mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
(Billing) [PM-27562] Create PremiumInterestStateService (#17139)
Creates a `PremiumInterestStateService` that manages state which conveys whether or not a user intends to setup a premium subscription. Implemented in Web only. No-op for other clients. This will apply for users who began the registration process on https://bitwarden.com/go/start-premium/, which is a marketing page designed to streamline users who intend to setup a premium subscription after registration.
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import {
|
||||
FakeAccountService,
|
||||
FakeStateProvider,
|
||||
mockAccountServiceWith,
|
||||
} from "@bitwarden/common/spec";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import {
|
||||
PREMIUM_INTEREST_KEY,
|
||||
WebPremiumInterestStateService,
|
||||
} from "./web-premium-interest-state.service";
|
||||
|
||||
describe("WebPremiumInterestStateService", () => {
|
||||
let service: WebPremiumInterestStateService;
|
||||
let stateProvider: FakeStateProvider;
|
||||
let accountService: FakeAccountService;
|
||||
|
||||
const mockUserId = newGuid() as UserId;
|
||||
const mockUserEmail = "user@example.com";
|
||||
|
||||
beforeEach(() => {
|
||||
accountService = mockAccountServiceWith(mockUserId, { email: mockUserEmail });
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
service = new WebPremiumInterestStateService(stateProvider);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("getPremiumInterest", () => {
|
||||
it("should throw an error when userId is not provided", async () => {
|
||||
const promise = service.getPremiumInterest(null);
|
||||
|
||||
await expect(promise).rejects.toThrow("UserId is required. Cannot get 'premiumInterest'.");
|
||||
});
|
||||
|
||||
it("should return null when no value is set", async () => {
|
||||
const result = await service.getPremiumInterest(mockUserId);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return true when value is set to true", async () => {
|
||||
await stateProvider.setUserState(PREMIUM_INTEREST_KEY, true, mockUserId);
|
||||
|
||||
const result = await service.getPremiumInterest(mockUserId);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when value is set to false", async () => {
|
||||
await stateProvider.setUserState(PREMIUM_INTEREST_KEY, false, mockUserId);
|
||||
|
||||
const result = await service.getPremiumInterest(mockUserId);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should use getUserState$ to retrieve the value", async () => {
|
||||
const getUserStateSpy = jest.spyOn(stateProvider, "getUserState$");
|
||||
await stateProvider.setUserState(PREMIUM_INTEREST_KEY, true, mockUserId);
|
||||
|
||||
await service.getPremiumInterest(mockUserId);
|
||||
|
||||
expect(getUserStateSpy).toHaveBeenCalledWith(PREMIUM_INTEREST_KEY, mockUserId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setPremiumInterest", () => {
|
||||
it("should throw an error when userId is not provided", async () => {
|
||||
const promise = service.setPremiumInterest(null, true);
|
||||
|
||||
await expect(promise).rejects.toThrow("UserId is required. Cannot set 'premiumInterest'.");
|
||||
});
|
||||
|
||||
it("should set the value to true", async () => {
|
||||
await service.setPremiumInterest(mockUserId, true);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
stateProvider.getUserState$(PREMIUM_INTEREST_KEY, mockUserId),
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should set the value to false", async () => {
|
||||
await service.setPremiumInterest(mockUserId, false);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
stateProvider.getUserState$(PREMIUM_INTEREST_KEY, mockUserId),
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should update an existing value", async () => {
|
||||
await service.setPremiumInterest(mockUserId, true);
|
||||
await service.setPremiumInterest(mockUserId, false);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
stateProvider.getUserState$(PREMIUM_INTEREST_KEY, mockUserId),
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should use setUserState to store the value", async () => {
|
||||
const setUserStateSpy = jest.spyOn(stateProvider, "setUserState");
|
||||
|
||||
await service.setPremiumInterest(mockUserId, true);
|
||||
|
||||
expect(setUserStateSpy).toHaveBeenCalledWith(PREMIUM_INTEREST_KEY, true, mockUserId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearPremiumInterest", () => {
|
||||
it("should throw an error when userId is not provided", async () => {
|
||||
const promise = service.clearPremiumInterest(null);
|
||||
|
||||
await expect(promise).rejects.toThrow("UserId is required. Cannot clear 'premiumInterest'.");
|
||||
});
|
||||
|
||||
it("should clear the value by setting it to null", async () => {
|
||||
await service.setPremiumInterest(mockUserId, true);
|
||||
await service.clearPremiumInterest(mockUserId);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
stateProvider.getUserState$(PREMIUM_INTEREST_KEY, mockUserId),
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should use setUserState with null to clear the value", async () => {
|
||||
const setUserStateSpy = jest.spyOn(stateProvider, "setUserState");
|
||||
await service.setPremiumInterest(mockUserId, true);
|
||||
|
||||
await service.clearPremiumInterest(mockUserId);
|
||||
|
||||
expect(setUserStateSpy).toHaveBeenCalledWith(PREMIUM_INTEREST_KEY, null, mockUserId);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { PremiumInterestStateService } from "@bitwarden/angular/billing/services/premium-interest/premium-interest-state.service.abstraction";
|
||||
import { BILLING_MEMORY, StateProvider, UserKeyDefinition } from "@bitwarden/state";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
export const PREMIUM_INTEREST_KEY = new UserKeyDefinition<boolean>(
|
||||
BILLING_MEMORY,
|
||||
"premiumInterest",
|
||||
{
|
||||
deserializer: (value: boolean) => value,
|
||||
clearOn: ["lock", "logout"],
|
||||
},
|
||||
);
|
||||
|
||||
@Injectable()
|
||||
export class WebPremiumInterestStateService implements PremiumInterestStateService {
|
||||
constructor(private stateProvider: StateProvider) {}
|
||||
|
||||
async getPremiumInterest(userId: UserId): Promise<boolean | null> {
|
||||
if (!userId) {
|
||||
throw new Error("UserId is required. Cannot get 'premiumInterest'.");
|
||||
}
|
||||
|
||||
return await firstValueFrom(this.stateProvider.getUserState$(PREMIUM_INTEREST_KEY, userId));
|
||||
}
|
||||
|
||||
async setPremiumInterest(userId: UserId, premiumInterest: boolean): Promise<void> {
|
||||
if (!userId) {
|
||||
throw new Error("UserId is required. Cannot set 'premiumInterest'.");
|
||||
}
|
||||
|
||||
await this.stateProvider.setUserState(PREMIUM_INTEREST_KEY, premiumInterest, userId);
|
||||
}
|
||||
|
||||
async clearPremiumInterest(userId: UserId): Promise<void> {
|
||||
if (!userId) {
|
||||
throw new Error("UserId is required. Cannot clear 'premiumInterest'.");
|
||||
}
|
||||
|
||||
await this.stateProvider.setUserState(PREMIUM_INTEREST_KEY, null, userId);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { DefaultDeviceManagementComponentService } from "@bitwarden/angular/auth
|
||||
import { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/auth/device-management/device-management-component.service.abstraction";
|
||||
import { ChangePasswordService } from "@bitwarden/angular/auth/password-management/change-password";
|
||||
import { SetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction";
|
||||
import { PremiumInterestStateService } from "@bitwarden/angular/billing/services/premium-interest/premium-interest-state.service.abstraction";
|
||||
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||
import {
|
||||
CLIENT_TYPE,
|
||||
@@ -129,6 +130,7 @@ import {
|
||||
WebSetInitialPasswordService,
|
||||
} from "../auth";
|
||||
import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service";
|
||||
import { WebPremiumInterestStateService } from "../billing/services/premium-interest/web-premium-interest-state.service";
|
||||
import { HtmlStorageService } from "../core/html-storage.service";
|
||||
import { I18nService } from "../core/i18n.service";
|
||||
import { WebFileDownloadService } from "../core/web-file-download.service";
|
||||
@@ -421,6 +423,11 @@ const safeProviders: SafeProvider[] = [
|
||||
Router,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PremiumInterestStateService,
|
||||
useClass: WebPremiumInterestStateService,
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { PremiumInterestStateService } from "./premium-interest-state.service.abstraction";
|
||||
|
||||
@Injectable()
|
||||
export class NoopPremiumInterestStateService implements PremiumInterestStateService {
|
||||
async getPremiumInterest(userId: UserId): Promise<boolean | null> {
|
||||
return null;
|
||||
} // no-op
|
||||
async setPremiumInterest(userId: UserId, premiumInterest: boolean): Promise<void> {} // no-op
|
||||
async clearPremiumInterest(userId: UserId): Promise<void> {} // no-op
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
/**
|
||||
* A service that manages state which conveys whether or not a user has expressed interest
|
||||
* in setting up a premium subscription. This applies for users who began the registration
|
||||
* process on https://bitwarden.com/go/start-premium/, which is a marketing page designed
|
||||
* to streamline users who intend to setup a premium subscription after registration.
|
||||
* - Implemented in Web only. No-op for other clients.
|
||||
*/
|
||||
export abstract class PremiumInterestStateService {
|
||||
abstract getPremiumInterest(userId: UserId): Promise<boolean | null>;
|
||||
abstract setPremiumInterest(userId: UserId, premiumInterest: boolean): Promise<void>;
|
||||
abstract clearPremiumInterest(userId: UserId): Promise<void>;
|
||||
}
|
||||
@@ -380,6 +380,8 @@ import { DefaultSetInitialPasswordService } from "../auth/password-management/se
|
||||
import { SetInitialPasswordService } from "../auth/password-management/set-initial-password/set-initial-password.service.abstraction";
|
||||
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction";
|
||||
import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation";
|
||||
import { NoopPremiumInterestStateService } from "../billing/services/premium-interest/noop-premium-interest-state.service";
|
||||
import { PremiumInterestStateService } from "../billing/services/premium-interest/premium-interest-state.service.abstraction";
|
||||
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
|
||||
import { DocumentLangSetter } from "../platform/i18n";
|
||||
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
|
||||
@@ -1724,6 +1726,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultNewDeviceVerificationComponentService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PremiumInterestStateService,
|
||||
useClass: NoopPremiumInterestStateService,
|
||||
deps: [],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -40,6 +40,7 @@ export const AUTO_CONFIRM = new StateDefinition("autoConfirm", "disk");
|
||||
|
||||
// Billing
|
||||
export const BILLING_DISK = new StateDefinition("billing", "disk");
|
||||
export const BILLING_MEMORY = new StateDefinition("billing", "memory");
|
||||
|
||||
// Auth
|
||||
|
||||
|
||||
Reference in New Issue
Block a user