mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +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 { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/auth/device-management/device-management-component.service.abstraction";
|
||||||
import { ChangePasswordService } from "@bitwarden/angular/auth/password-management/change-password";
|
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 { 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 { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||||
import {
|
import {
|
||||||
CLIENT_TYPE,
|
CLIENT_TYPE,
|
||||||
@@ -129,6 +130,7 @@ import {
|
|||||||
WebSetInitialPasswordService,
|
WebSetInitialPasswordService,
|
||||||
} from "../auth";
|
} from "../auth";
|
||||||
import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service";
|
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 { HtmlStorageService } from "../core/html-storage.service";
|
||||||
import { I18nService } from "../core/i18n.service";
|
import { I18nService } from "../core/i18n.service";
|
||||||
import { WebFileDownloadService } from "../core/web-file-download.service";
|
import { WebFileDownloadService } from "../core/web-file-download.service";
|
||||||
@@ -421,6 +423,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
Router,
|
Router,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: PremiumInterestStateService,
|
||||||
|
useClass: WebPremiumInterestStateService,
|
||||||
|
deps: [StateProvider],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@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 { 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 as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction";
|
||||||
import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation";
|
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 { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
|
||||||
import { DocumentLangSetter } from "../platform/i18n";
|
import { DocumentLangSetter } from "../platform/i18n";
|
||||||
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
|
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
|
||||||
@@ -1724,6 +1726,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: DefaultNewDeviceVerificationComponentService,
|
useClass: DefaultNewDeviceVerificationComponentService,
|
||||||
deps: [],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: PremiumInterestStateService,
|
||||||
|
useClass: NoopPremiumInterestStateService,
|
||||||
|
deps: [],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export const AUTO_CONFIRM = new StateDefinition("autoConfirm", "disk");
|
|||||||
|
|
||||||
// Billing
|
// Billing
|
||||||
export const BILLING_DISK = new StateDefinition("billing", "disk");
|
export const BILLING_DISK = new StateDefinition("billing", "disk");
|
||||||
|
export const BILLING_MEMORY = new StateDefinition("billing", "memory");
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user