[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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
85 changed files with 942 additions and 261 deletions

View File

@ -9,6 +9,7 @@ import {
ApiServiceInitOptions, ApiServiceInitOptions,
} from "../../../platform/background/service-factories/api-service.factory"; } from "../../../platform/background/service-factories/api-service.factory";
import { appIdServiceFactory } from "../../../platform/background/service-factories/app-id-service.factory"; import { appIdServiceFactory } from "../../../platform/background/service-factories/app-id-service.factory";
import { billingAccountProfileStateServiceFactory } from "../../../platform/background/service-factories/billing-account-profile-state-service.factory";
import { import {
CryptoServiceInitOptions, CryptoServiceInitOptions,
cryptoServiceFactory, cryptoServiceFactory,
@ -119,6 +120,7 @@ export function loginStrategyServiceFactory(
await deviceTrustCryptoServiceFactory(cache, opts), await deviceTrustCryptoServiceFactory(cache, opts),
await authRequestServiceFactory(cache, opts), await authRequestServiceFactory(cache, opts),
await globalStateProviderFactory(cache, opts), await globalStateProviderFactory(cache, opts),
await billingAccountProfileStateServiceFactory(cache, opts),
), ),
); );
} }

View File

@ -6,6 +6,7 @@ import {
EventCollectionServiceInitOptions, EventCollectionServiceInitOptions,
eventCollectionServiceFactory, eventCollectionServiceFactory,
} from "../../../background/service-factories/event-collection-service.factory"; } from "../../../background/service-factories/event-collection-service.factory";
import { billingAccountProfileStateServiceFactory } from "../../../platform/background/service-factories/billing-account-profile-state-service.factory";
import { import {
CachedServices, CachedServices,
factory, factory,
@ -69,6 +70,7 @@ export function autofillServiceFactory(
await logServiceFactory(cache, opts), await logServiceFactory(cache, opts),
await domainSettingsServiceFactory(cache, opts), await domainSettingsServiceFactory(cache, opts),
await userVerificationServiceFactory(cache, opts), await userVerificationServiceFactory(cache, opts),
await billingAccountProfileStateServiceFactory(cache, opts),
), ),
); );
} }

View File

@ -3,6 +3,7 @@ import { of } from "rxjs";
import { NOOP_COMMAND_SUFFIX } from "@bitwarden/common/autofill/constants"; import { NOOP_COMMAND_SUFFIX } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
@ -18,6 +19,7 @@ describe("context-menu", () => {
let autofillSettingsService: MockProxy<AutofillSettingsServiceAbstraction>; let autofillSettingsService: MockProxy<AutofillSettingsServiceAbstraction>;
let i18nService: MockProxy<I18nService>; let i18nService: MockProxy<I18nService>;
let logService: MockProxy<LogService>; let logService: MockProxy<LogService>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let removeAllSpy: jest.SpyInstance<void, [callback?: () => void]>; let removeAllSpy: jest.SpyInstance<void, [callback?: () => void]>;
let createSpy: jest.SpyInstance< let createSpy: jest.SpyInstance<
@ -32,6 +34,7 @@ describe("context-menu", () => {
autofillSettingsService = mock(); autofillSettingsService = mock();
i18nService = mock(); i18nService = mock();
logService = mock(); logService = mock();
billingAccountProfileStateService = mock();
removeAllSpy = jest removeAllSpy = jest
.spyOn(chrome.contextMenus, "removeAll") .spyOn(chrome.contextMenus, "removeAll")
@ -50,6 +53,7 @@ describe("context-menu", () => {
autofillSettingsService, autofillSettingsService,
i18nService, i18nService,
logService, logService,
billingAccountProfileStateService,
); );
autofillSettingsService.enableContextMenu$ = of(true); autofillSettingsService.enableContextMenu$ = of(true);
}); });
@ -66,7 +70,7 @@ describe("context-menu", () => {
}); });
it("has menu enabled, but does not have premium", async () => { it("has menu enabled, but does not have premium", async () => {
stateService.getCanAccessPremium.mockResolvedValue(false); billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false);
const createdMenu = await sut.init(); const createdMenu = await sut.init();
expect(createdMenu).toBeTruthy(); expect(createdMenu).toBeTruthy();
@ -74,7 +78,7 @@ describe("context-menu", () => {
}); });
it("has menu enabled and has premium", async () => { it("has menu enabled and has premium", async () => {
stateService.getCanAccessPremium.mockResolvedValue(true); billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
const createdMenu = await sut.init(); const createdMenu = await sut.init();
expect(createdMenu).toBeTruthy(); expect(createdMenu).toBeTruthy();
@ -128,7 +132,7 @@ describe("context-menu", () => {
}); });
it("create entry for each cipher piece", async () => { it("create entry for each cipher piece", async () => {
stateService.getCanAccessPremium.mockResolvedValue(true); billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
await sut.loadOptions("TEST_TITLE", "1", createCipher()); await sut.loadOptions("TEST_TITLE", "1", createCipher());
@ -137,7 +141,7 @@ describe("context-menu", () => {
}); });
it("creates a login/unlock item for each context menu action option when user is not authenticated", async () => { it("creates a login/unlock item for each context menu action option when user is not authenticated", async () => {
stateService.getCanAccessPremium.mockResolvedValue(true); billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
await sut.loadOptions("TEST_TITLE", "NOOP"); await sut.loadOptions("TEST_TITLE", "NOOP");

View File

@ -17,6 +17,7 @@ import {
SEPARATOR_ID, SEPARATOR_ID,
} from "@bitwarden/common/autofill/constants"; } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
@ -27,6 +28,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { autofillSettingsServiceFactory } from "../../autofill/background/service_factories/autofill-settings-service.factory"; import { autofillSettingsServiceFactory } from "../../autofill/background/service_factories/autofill-settings-service.factory";
import { Account } from "../../models/account"; import { Account } from "../../models/account";
import { billingAccountProfileStateServiceFactory } from "../../platform/background/service-factories/billing-account-profile-state-service.factory";
import { CachedServices } from "../../platform/background/service-factories/factory-options"; import { CachedServices } from "../../platform/background/service-factories/factory-options";
import { import {
i18nServiceFactory, i18nServiceFactory,
@ -163,6 +165,7 @@ export class MainContextMenuHandler {
private autofillSettingsService: AutofillSettingsServiceAbstraction, private autofillSettingsService: AutofillSettingsServiceAbstraction,
private i18nService: I18nService, private i18nService: I18nService,
private logService: LogService, private logService: LogService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) {} ) {}
static async mv3Create(cachedServices: CachedServices) { static async mv3Create(cachedServices: CachedServices) {
@ -196,6 +199,7 @@ export class MainContextMenuHandler {
await autofillSettingsServiceFactory(cachedServices, serviceOptions), await autofillSettingsServiceFactory(cachedServices, serviceOptions),
await i18nServiceFactory(cachedServices, serviceOptions), await i18nServiceFactory(cachedServices, serviceOptions),
await logServiceFactory(cachedServices, serviceOptions), await logServiceFactory(cachedServices, serviceOptions),
await billingAccountProfileStateServiceFactory(cachedServices, serviceOptions),
); );
} }
@ -217,7 +221,10 @@ export class MainContextMenuHandler {
try { try {
for (const options of this.initContextMenuItems) { for (const options of this.initContextMenuItems) {
if (options.checkPremiumAccess && !(await this.stateService.getCanAccessPremium())) { if (
options.checkPremiumAccess &&
!(await firstValueFrom(this.billingAccountProfileStateService.hasPremiumFromAnySource$))
) {
continue; continue;
} }
@ -312,7 +319,9 @@ export class MainContextMenuHandler {
await createChildItem(COPY_USERNAME_ID); await createChildItem(COPY_USERNAME_ID);
} }
const canAccessPremium = await this.stateService.getCanAccessPremium(); const canAccessPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
);
if (canAccessPremium && (!cipher || !Utils.isNullOrEmpty(cipher.login?.totp))) { if (canAccessPremium && (!cipher || !Utils.isNullOrEmpty(cipher.login?.totp))) {
await createChildItem(COPY_VERIFICATION_CODE_ID); await createChildItem(COPY_VERIFICATION_CODE_ID);
} }

View File

@ -8,6 +8,7 @@ import {
DefaultDomainSettingsService, DefaultDomainSettingsService,
DomainSettingsService, DomainSettingsService,
} from "@bitwarden/common/autofill/services/domain-settings.service"; } from "@bitwarden/common/autofill/services/domain-settings.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -72,6 +73,7 @@ describe("AutofillService", () => {
const eventCollectionService = mock<EventCollectionService>(); const eventCollectionService = mock<EventCollectionService>();
const logService = mock<LogService>(); const logService = mock<LogService>();
const userVerificationService = mock<UserVerificationService>(); const userVerificationService = mock<UserVerificationService>();
const billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
beforeEach(() => { beforeEach(() => {
autofillService = new AutofillService( autofillService = new AutofillService(
@ -83,6 +85,7 @@ describe("AutofillService", () => {
logService, logService,
domainSettingsService, domainSettingsService,
userVerificationService, userVerificationService,
billingAccountProfileStateService,
); );
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
@ -476,6 +479,7 @@ describe("AutofillService", () => {
it("throws an error if an autofill did not occur for any of the passed pages", async () => { it("throws an error if an autofill did not occur for any of the passed pages", async () => {
autofillOptions.tab.url = "https://a-different-url.com"; autofillOptions.tab.url = "https://a-different-url.com";
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
try { try {
await autofillService.doAutoFill(autofillOptions); await autofillService.doAutoFill(autofillOptions);
@ -487,7 +491,6 @@ describe("AutofillService", () => {
}); });
it("will autofill login data for a page", async () => { it("will autofill login data for a page", async () => {
jest.spyOn(stateService, "getCanAccessPremium");
jest.spyOn(autofillService as any, "generateFillScript"); jest.spyOn(autofillService as any, "generateFillScript");
jest.spyOn(autofillService as any, "generateLoginFillScript"); jest.spyOn(autofillService as any, "generateLoginFillScript");
jest.spyOn(logService, "info"); jest.spyOn(logService, "info");
@ -497,8 +500,6 @@ describe("AutofillService", () => {
const autofillResult = await autofillService.doAutoFill(autofillOptions); const autofillResult = await autofillService.doAutoFill(autofillOptions);
const currentAutofillPageDetails = autofillOptions.pageDetails[0]; const currentAutofillPageDetails = autofillOptions.pageDetails[0];
expect(stateService.getCanAccessPremium).toHaveBeenCalled();
expect(autofillService["getDefaultUriMatchStrategy"]).toHaveBeenCalled();
expect(autofillService["generateFillScript"]).toHaveBeenCalledWith( expect(autofillService["generateFillScript"]).toHaveBeenCalledWith(
currentAutofillPageDetails.details, currentAutofillPageDetails.details,
{ {
@ -660,7 +661,7 @@ describe("AutofillService", () => {
it("returns a TOTP value", async () => { it("returns a TOTP value", async () => {
const totpCode = "123456"; const totpCode = "123456";
autofillOptions.cipher.login.totp = "totp"; autofillOptions.cipher.login.totp = "totp";
jest.spyOn(stateService, "getCanAccessPremium").mockResolvedValue(true); billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(true); jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(true);
jest.spyOn(totpService, "getCode").mockResolvedValue(totpCode); jest.spyOn(totpService, "getCode").mockResolvedValue(totpCode);
@ -673,7 +674,7 @@ describe("AutofillService", () => {
it("does not return a TOTP value if the user does not have premium features", async () => { it("does not return a TOTP value if the user does not have premium features", async () => {
autofillOptions.cipher.login.totp = "totp"; autofillOptions.cipher.login.totp = "totp";
jest.spyOn(stateService, "getCanAccessPremium").mockResolvedValue(false); billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false);
jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(true); jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(true);
const autofillResult = await autofillService.doAutoFill(autofillOptions); const autofillResult = await autofillService.doAutoFill(autofillOptions);
@ -707,7 +708,7 @@ describe("AutofillService", () => {
it("returns a null value if the user cannot access premium and the organization does not use TOTP", async () => { it("returns a null value if the user cannot access premium and the organization does not use TOTP", async () => {
autofillOptions.cipher.login.totp = "totp"; autofillOptions.cipher.login.totp = "totp";
autofillOptions.cipher.organizationUseTotp = false; autofillOptions.cipher.organizationUseTotp = false;
jest.spyOn(stateService, "getCanAccessPremium").mockResolvedValueOnce(false); billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false);
const autofillResult = await autofillService.doAutoFill(autofillOptions); const autofillResult = await autofillService.doAutoFill(autofillOptions);
@ -717,13 +718,12 @@ describe("AutofillService", () => {
it("returns a null value if the user has disabled `auto TOTP copy`", async () => { it("returns a null value if the user has disabled `auto TOTP copy`", async () => {
autofillOptions.cipher.login.totp = "totp"; autofillOptions.cipher.login.totp = "totp";
autofillOptions.cipher.organizationUseTotp = true; autofillOptions.cipher.organizationUseTotp = true;
jest.spyOn(stateService, "getCanAccessPremium").mockResolvedValue(true); billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(false); jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(false);
jest.spyOn(totpService, "getCode"); jest.spyOn(totpService, "getCode");
const autofillResult = await autofillService.doAutoFill(autofillOptions); const autofillResult = await autofillService.doAutoFill(autofillOptions);
expect(stateService.getCanAccessPremium).toHaveBeenCalled();
expect(autofillService.getShouldAutoCopyTotp).toHaveBeenCalled(); expect(autofillService.getShouldAutoCopyTotp).toHaveBeenCalled();
expect(totpService.getCode).not.toHaveBeenCalled(); expect(totpService.getCode).not.toHaveBeenCalled();
expect(autofillResult).toBeNull(); expect(autofillResult).toBeNull();

View File

@ -5,6 +5,7 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { import {
UriMatchStrategySetting, UriMatchStrategySetting,
@ -55,6 +56,7 @@ export default class AutofillService implements AutofillServiceInterface {
private logService: LogService, private logService: LogService,
private domainSettingsService: DomainSettingsService, private domainSettingsService: DomainSettingsService,
private userVerificationService: UserVerificationService, private userVerificationService: UserVerificationService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) {} ) {}
/** /**
@ -240,7 +242,9 @@ export default class AutofillService implements AutofillServiceInterface {
let totp: string | null = null; let totp: string | null = null;
const canAccessPremium = await this.stateService.getCanAccessPremium(); const canAccessPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
);
const defaultUriMatch = await this.getDefaultUriMatchStrategy(); const defaultUriMatch = await this.getDefaultUriMatchStrategy();
if (!canAccessPremium) { if (!canAccessPremium) {

View File

@ -64,6 +64,8 @@ import {
UserNotificationSettingsService, UserNotificationSettingsService,
UserNotificationSettingsServiceAbstraction, UserNotificationSettingsServiceAbstraction,
} from "@bitwarden/common/autofill/services/user-notification-settings.service"; } from "@bitwarden/common/autofill/services/user-notification-settings.service";
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 { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -311,6 +313,7 @@ export default class MainBackground {
biometricStateService: BiometricStateService; biometricStateService: BiometricStateService;
stateEventRunnerService: StateEventRunnerService; stateEventRunnerService: StateEventRunnerService;
ssoLoginService: SsoLoginServiceAbstraction; ssoLoginService: SsoLoginServiceAbstraction;
billingAccountProfileStateService: BillingAccountProfileStateService;
onUpdatedRan: boolean; onUpdatedRan: boolean;
onReplacedRan: boolean; onReplacedRan: boolean;
@ -572,6 +575,10 @@ export default class MainBackground {
this.stateService, this.stateService,
); );
this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService(
this.activeUserStateProvider,
);
this.loginStrategyService = new LoginStrategyService( this.loginStrategyService = new LoginStrategyService(
this.cryptoService, this.cryptoService,
this.apiService, this.apiService,
@ -591,6 +598,7 @@ export default class MainBackground {
this.deviceTrustCryptoService, this.deviceTrustCryptoService,
this.authRequestService, this.authRequestService,
this.globalStateProvider, this.globalStateProvider,
this.billingAccountProfileStateService,
); );
this.ssoLoginService = new SsoLoginService(this.stateProvider); this.ssoLoginService = new SsoLoginService(this.stateProvider);
@ -718,6 +726,7 @@ export default class MainBackground {
this.sendApiService, this.sendApiService,
this.avatarService, this.avatarService,
logoutCallback, logoutCallback,
this.billingAccountProfileStateService,
); );
this.eventUploadService = new EventUploadService( this.eventUploadService = new EventUploadService(
this.apiService, this.apiService,
@ -741,6 +750,7 @@ export default class MainBackground {
this.logService, this.logService,
this.domainSettingsService, this.domainSettingsService,
this.userVerificationService, this.userVerificationService,
this.billingAccountProfileStateService,
); );
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
@ -961,6 +971,7 @@ export default class MainBackground {
this.autofillSettingsService, this.autofillSettingsService,
this.i18nService, this.i18nService,
this.logService, this.logService,
this.billingAccountProfileStateService,
); );
this.cipherContextMenuHandler = new CipherContextMenuHandler( this.cipherContextMenuHandler = new CipherContextMenuHandler(

View File

@ -0,0 +1,28 @@
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";
type BillingAccountProfileStateServiceFactoryOptions = FactoryOptions;
export type BillingAccountProfileStateServiceInitOptions =
BillingAccountProfileStateServiceFactoryOptions & StateProviderInitOptions;
export function billingAccountProfileStateServiceFactory(
cache: {
billingAccountProfileStateService?: BillingAccountProfileStateService;
} & CachedServices,
opts: BillingAccountProfileStateServiceInitOptions,
): Promise<BillingAccountProfileStateService> {
return factory(
cache,
"billingAccountProfileStateService",
opts,
async () =>
new DefaultBillingAccountProfileStateService(
await activeUserStateProviderFactory(cache, opts),
),
);
}

View File

@ -12,7 +12,7 @@
</header> </header>
<main tabindex="-1"> <main tabindex="-1">
<div class="content"> <div class="content">
<ng-container *ngIf="!isPremium"> <ng-container *ngIf="!(isPremium$ | async)">
<p class="text-center lead">{{ "premiumNotCurrentMember" | i18n }}</p> <p class="text-center lead">{{ "premiumNotCurrentMember" | i18n }}</p>
<p>{{ "premiumSignUpAndGet" | i18n }}</p> <p>{{ "premiumSignUpAndGet" | i18n }}</p>
<ul class="bwi-ul"> <ul class="bwi-ul">
@ -61,7 +61,7 @@
></i> ></i>
</button> </button>
</ng-container> </ng-container>
<ng-container *ngIf="isPremium"> <ng-container *ngIf="isPremium$ | async">
<p class="text-center lead">{{ "premiumCurrentMember" | i18n }}</p> <p class="text-center lead">{{ "premiumCurrentMember" | i18n }}</p>
<p class="text-center">{{ "premiumCurrentMemberThanks" | i18n }}</p> <p class="text-center">{{ "premiumCurrentMemberThanks" | i18n }}</p>
<button type="button" class="btn block primary" (click)="manage()"> <button type="button" class="btn block primary" (click)="manage()">

View File

@ -3,6 +3,7 @@ import { Component } from "@angular/core";
import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/vault/components/premium.component"; import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/vault/components/premium.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -27,6 +28,7 @@ export class PremiumComponent extends BasePremiumComponent {
private currencyPipe: CurrencyPipe, private currencyPipe: CurrencyPipe,
dialogService: DialogService, dialogService: DialogService,
environmentService: EnvironmentService, environmentService: EnvironmentService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
i18nService, i18nService,
@ -36,6 +38,7 @@ export class PremiumComponent extends BasePremiumComponent {
stateService, stateService,
dialogService, dialogService,
environmentService, environmentService,
billingAccountProfileStateService,
); );
// Support old price string. Can be removed in future once all translations are properly updated. // Support old price string. Can be removed in future once all translations are properly updated.

View File

@ -6,6 +6,7 @@ import { first } from "rxjs/operators";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -49,6 +50,7 @@ export class SendAddEditComponent extends BaseAddEditComponent {
dialogService: DialogService, dialogService: DialogService,
formBuilder: FormBuilder, formBuilder: FormBuilder,
private filePopoutUtilsService: FilePopoutUtilsService, private filePopoutUtilsService: FilePopoutUtilsService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
i18nService, i18nService,
@ -63,6 +65,7 @@ export class SendAddEditComponent extends BaseAddEditComponent {
sendApiService, sendApiService,
dialogService, dialogService,
formBuilder, formBuilder,
billingAccountProfileStateService,
); );
} }

View File

@ -1,10 +1,11 @@
import { Component, EventEmitter, Input, Output } from "@angular/core"; import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
@ -15,7 +16,7 @@ import { PasswordRepromptService } from "@bitwarden/vault";
selector: "app-action-buttons", selector: "app-action-buttons",
templateUrl: "action-buttons.component.html", templateUrl: "action-buttons.component.html",
}) })
export class ActionButtonsComponent { export class ActionButtonsComponent implements OnInit, OnDestroy {
@Output() onView = new EventEmitter<CipherView>(); @Output() onView = new EventEmitter<CipherView>();
@Output() launchEvent = new EventEmitter<CipherView>(); @Output() launchEvent = new EventEmitter<CipherView>();
@Input() cipher: CipherView; @Input() cipher: CipherView;
@ -24,17 +25,28 @@ export class ActionButtonsComponent {
cipherType = CipherType; cipherType = CipherType;
userHasPremiumAccess = false; userHasPremiumAccess = false;
private componentIsDestroyed$ = new Subject<boolean>();
constructor( constructor(
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private eventCollectionService: EventCollectionService, private eventCollectionService: EventCollectionService,
private totpService: TotpService, private totpService: TotpService,
private stateService: StateService,
private passwordRepromptService: PasswordRepromptService, private passwordRepromptService: PasswordRepromptService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) {} ) {}
async ngOnInit() { ngOnInit() {
this.userHasPremiumAccess = await this.stateService.getCanAccessPremium(); this.billingAccountProfileStateService.hasPremiumFromAnySource$
.pipe(takeUntil(this.componentIsDestroyed$))
.subscribe((canAccessPremium: boolean) => {
this.userHasPremiumAccess = canAccessPremium;
});
}
ngOnDestroy() {
this.componentIsDestroyed$.next(true);
this.componentIsDestroyed$.complete();
} }
launchCipher() { launchCipher() {

View File

@ -5,6 +5,7 @@ import { first } from "rxjs/operators";
import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component"; import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -34,6 +35,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
logService: LogService, logService: LogService,
fileDownloadService: FileDownloadService, fileDownloadService: FileDownloadService,
dialogService: DialogService, dialogService: DialogService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
cipherService, cipherService,
@ -46,6 +48,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
stateService, stateService,
fileDownloadService, fileDownloadService,
dialogService, dialogService,
billingAccountProfileStateService,
); );
} }

View File

@ -9,6 +9,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
@ -95,6 +96,7 @@ export class ViewComponent extends BaseViewComponent {
fileDownloadService: FileDownloadService, fileDownloadService: FileDownloadService,
dialogService: DialogService, dialogService: DialogService,
datePipe: DatePipe, datePipe: DatePipe,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
cipherService, cipherService,
@ -117,6 +119,7 @@ export class ViewComponent extends BaseViewComponent {
fileDownloadService, fileDownloadService,
dialogService, dialogService,
datePipe, datePipe,
billingAccountProfileStateService,
); );
} }

View File

@ -41,6 +41,8 @@ import {
DefaultDomainSettingsService, DefaultDomainSettingsService,
DomainSettingsService, DomainSettingsService,
} from "@bitwarden/common/autofill/services/domain-settings.service"; } from "@bitwarden/common/autofill/services/domain-settings.service";
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 { ClientType } from "@bitwarden/common/enums"; import { ClientType } from "@bitwarden/common/enums";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
@ -221,6 +223,7 @@ export class Main {
avatarService: AvatarServiceAbstraction; avatarService: AvatarServiceAbstraction;
stateEventRunnerService: StateEventRunnerService; stateEventRunnerService: StateEventRunnerService;
biometricStateService: BiometricStateService; biometricStateService: BiometricStateService;
billingAccountProfileStateService: BillingAccountProfileStateService;
constructor() { constructor() {
let p = null; let p = null;
@ -456,6 +459,10 @@ export class Main {
this.stateService, this.stateService,
); );
this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService(
this.activeUserStateProvider,
);
this.loginStrategyService = new LoginStrategyService( this.loginStrategyService = new LoginStrategyService(
this.cryptoService, this.cryptoService,
this.apiService, this.apiService,
@ -475,6 +482,7 @@ export class Main {
this.deviceTrustCryptoService, this.deviceTrustCryptoService,
this.authRequestService, this.authRequestService,
this.globalStateProvider, this.globalStateProvider,
this.billingAccountProfileStateService,
); );
this.authService = new AuthService( this.authService = new AuthService(
@ -586,6 +594,7 @@ export class Main {
this.sendApiService, this.sendApiService,
this.avatarService, this.avatarService,
async (expired: boolean) => await this.logout(), async (expired: boolean) => await this.logout(),
this.billingAccountProfileStateService,
); );
this.totpService = new TotpService(this.cryptoFunctionService, this.logService); this.totpService = new TotpService(this.cryptoFunctionService, this.logService);

View File

@ -1,9 +1,12 @@
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { CardExport } from "@bitwarden/common/models/export/card.export"; import { CardExport } from "@bitwarden/common/models/export/card.export";
import { CipherExport } from "@bitwarden/common/models/export/cipher.export"; import { CipherExport } from "@bitwarden/common/models/export/cipher.export";
@ -57,6 +60,7 @@ export class GetCommand extends DownloadCommand {
private apiService: ApiService, private apiService: ApiService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
private eventCollectionService: EventCollectionService, private eventCollectionService: EventCollectionService,
private accountProfileService: BillingAccountProfileStateService,
) { ) {
super(cryptoService); super(cryptoService);
} }
@ -251,7 +255,9 @@ export class GetCommand extends DownloadCommand {
return Response.error("Couldn't generate TOTP code."); return Response.error("Couldn't generate TOTP code.");
} }
const canAccessPremium = await this.stateService.getCanAccessPremium(); const canAccessPremium = await firstValueFrom(
this.accountProfileService.hasPremiumFromAnySource$,
);
if (!canAccessPremium) { if (!canAccessPremium) {
const originalCipher = await this.cipherService.get(cipher.id); const originalCipher = await this.cipherService.get(cipher.id);
if ( if (
@ -334,7 +340,10 @@ export class GetCommand extends DownloadCommand {
return Response.multipleResults(attachments.map((a) => a.id)); return Response.multipleResults(attachments.map((a) => a.id));
} }
if (!(await this.stateService.getCanAccessPremium())) { const canAccessPremium = await firstValueFrom(
this.accountProfileService.hasPremiumFromAnySource$,
);
if (!canAccessPremium) {
const originalCipher = await this.cipherService.get(cipher.id); const originalCipher = await this.cipherService.get(cipher.id);
if (originalCipher == null || originalCipher.organizationId == null) { if (originalCipher == null || originalCipher.organizationId == null) {
return Response.error("Premium status is required to use this feature."); return Response.error("Premium status is required to use this feature.");

View File

@ -68,6 +68,7 @@ export class ServeCommand {
this.main.apiService, this.main.apiService,
this.main.organizationService, this.main.organizationService,
this.main.eventCollectionService, this.main.eventCollectionService,
this.main.billingAccountProfileStateService,
); );
this.listCommand = new ListCommand( this.listCommand = new ListCommand(
this.main.cipherService, this.main.cipherService,
@ -82,10 +83,10 @@ export class ServeCommand {
this.createCommand = new CreateCommand( this.createCommand = new CreateCommand(
this.main.cipherService, this.main.cipherService,
this.main.folderService, this.main.folderService,
this.main.stateService,
this.main.cryptoService, this.main.cryptoService,
this.main.apiService, this.main.apiService,
this.main.folderApiService, this.main.folderApiService,
this.main.billingAccountProfileStateService,
); );
this.editCommand = new EditCommand( this.editCommand = new EditCommand(
this.main.cipherService, this.main.cipherService,
@ -108,9 +109,9 @@ export class ServeCommand {
this.deleteCommand = new DeleteCommand( this.deleteCommand = new DeleteCommand(
this.main.cipherService, this.main.cipherService,
this.main.folderService, this.main.folderService,
this.main.stateService,
this.main.apiService, this.main.apiService,
this.main.folderApiService, this.main.folderApiService,
this.main.billingAccountProfileStateService,
); );
this.confirmCommand = new ConfirmCommand( this.confirmCommand = new ConfirmCommand(
this.main.apiService, this.main.apiService,
@ -135,9 +136,9 @@ export class ServeCommand {
this.sendCreateCommand = new SendCreateCommand( this.sendCreateCommand = new SendCreateCommand(
this.main.sendService, this.main.sendService,
this.main.stateService,
this.main.environmentService, this.main.environmentService,
this.main.sendApiService, this.main.sendApiService,
this.main.billingAccountProfileStateService,
); );
this.sendDeleteCommand = new SendDeleteCommand(this.main.sendService, this.main.sendApiService); this.sendDeleteCommand = new SendDeleteCommand(this.main.sendService, this.main.sendApiService);
this.sendGetCommand = new SendGetCommand( this.sendGetCommand = new SendGetCommand(
@ -148,9 +149,9 @@ export class ServeCommand {
); );
this.sendEditCommand = new SendEditCommand( this.sendEditCommand = new SendEditCommand(
this.main.sendService, this.main.sendService,
this.main.stateService,
this.sendGetCommand, this.sendGetCommand,
this.main.sendApiService, this.main.sendApiService,
this.main.billingAccountProfileStateService,
); );
this.sendListCommand = new SendListCommand( this.sendListCommand = new SendListCommand(
this.main.sendService, this.main.sendService,

View File

@ -1,8 +1,10 @@
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; import * as path from "path";
import { firstValueFrom } from "rxjs";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
@ -16,9 +18,9 @@ import { SendResponse } from "../models/send.response";
export class SendCreateCommand { export class SendCreateCommand {
constructor( constructor(
private sendService: SendService, private sendService: SendService,
private stateService: StateService,
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private sendApiService: SendApiService, private sendApiService: SendApiService,
private accountProfileService: BillingAccountProfileStateService,
) {} ) {}
async run(requestJson: any, cmdOptions: Record<string, any>) { async run(requestJson: any, cmdOptions: Record<string, any>) {
@ -82,7 +84,7 @@ export class SendCreateCommand {
); );
} }
if (!(await this.stateService.getCanAccessPremium())) { if (!(await firstValueFrom(this.accountProfileService.hasPremiumFromAnySource$))) {
return Response.error("Premium status is required to use this feature."); return Response.error("Premium status is required to use this feature.");
} }

View File

@ -1,4 +1,6 @@
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { firstValueFrom } from "rxjs";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
@ -12,9 +14,9 @@ import { SendGetCommand } from "./get.command";
export class SendEditCommand { export class SendEditCommand {
constructor( constructor(
private sendService: SendService, private sendService: SendService,
private stateService: StateService,
private getCommand: SendGetCommand, private getCommand: SendGetCommand,
private sendApiService: SendApiService, private sendApiService: SendApiService,
private accountProfileService: BillingAccountProfileStateService,
) {} ) {}
async run(requestJson: string, cmdOptions: Record<string, any>): Promise<Response> { async run(requestJson: string, cmdOptions: Record<string, any>): Promise<Response> {
@ -57,7 +59,10 @@ export class SendEditCommand {
return Response.badRequest("Cannot change a Send's type"); return Response.badRequest("Cannot change a Send's type");
} }
if (send.type === SendType.File && !(await this.stateService.getCanAccessPremium())) { const canAccessPremium = await firstValueFrom(
this.accountProfileService.hasPremiumFromAnySource$,
);
if (send.type === SendType.File && !canAccessPremium) {
return Response.error("Premium status is required to use this feature."); return Response.error("Premium status is required to use this feature.");
} }

View File

@ -153,6 +153,7 @@ export class SendProgram extends Program {
this.main.apiService, this.main.apiService,
this.main.organizationService, this.main.organizationService,
this.main.eventCollectionService, this.main.eventCollectionService,
this.main.billingAccountProfileStateService,
); );
const response = await cmd.run("template", object, null); const response = await cmd.run("template", object, null);
this.processResponse(response); this.processResponse(response);
@ -253,9 +254,9 @@ export class SendProgram extends Program {
); );
const cmd = new SendEditCommand( const cmd = new SendEditCommand(
this.main.sendService, this.main.sendService,
this.main.stateService,
getCmd, getCmd,
this.main.sendApiService, this.main.sendApiService,
this.main.billingAccountProfileStateService,
); );
const response = await cmd.run(encodedJson, options); const response = await cmd.run(encodedJson, options);
this.processResponse(response); this.processResponse(response);
@ -323,9 +324,9 @@ export class SendProgram extends Program {
await this.exitIfLocked(); await this.exitIfLocked();
const cmd = new SendCreateCommand( const cmd = new SendCreateCommand(
this.main.sendService, this.main.sendService,
this.main.stateService,
this.main.environmentService, this.main.environmentService,
this.main.sendApiService, this.main.sendApiService,
this.main.billingAccountProfileStateService,
); );
return await cmd.run(encodedJson, options); return await cmd.run(encodedJson, options);
} }

View File

@ -188,6 +188,7 @@ export class VaultProgram extends Program {
this.main.apiService, this.main.apiService,
this.main.organizationService, this.main.organizationService,
this.main.eventCollectionService, this.main.eventCollectionService,
this.main.billingAccountProfileStateService,
); );
const response = await command.run(object, id, cmd); const response = await command.run(object, id, cmd);
this.processResponse(response); this.processResponse(response);
@ -226,10 +227,10 @@ export class VaultProgram extends Program {
const command = new CreateCommand( const command = new CreateCommand(
this.main.cipherService, this.main.cipherService,
this.main.folderService, this.main.folderService,
this.main.stateService,
this.main.cryptoService, this.main.cryptoService,
this.main.apiService, this.main.apiService,
this.main.folderApiService, this.main.folderApiService,
this.main.billingAccountProfileStateService,
); );
const response = await command.run(object, encodedJson, cmd); const response = await command.run(object, encodedJson, cmd);
this.processResponse(response); this.processResponse(response);
@ -313,9 +314,9 @@ export class VaultProgram extends Program {
const command = new DeleteCommand( const command = new DeleteCommand(
this.main.cipherService, this.main.cipherService,
this.main.folderService, this.main.folderService,
this.main.stateService,
this.main.apiService, this.main.apiService,
this.main.folderApiService, this.main.folderApiService,
this.main.billingAccountProfileStateService,
); );
const response = await command.run(object, id, cmd); const response = await command.run(object, id, cmd);
this.processResponse(response); this.processResponse(response);

View File

@ -1,13 +1,15 @@
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; import * as path from "path";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { CipherExport } from "@bitwarden/common/models/export/cipher.export"; import { CipherExport } from "@bitwarden/common/models/export/cipher.export";
import { CollectionExport } from "@bitwarden/common/models/export/collection.export"; import { CollectionExport } from "@bitwarden/common/models/export/collection.export";
import { FolderExport } from "@bitwarden/common/models/export/folder.export"; import { FolderExport } from "@bitwarden/common/models/export/folder.export";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
@ -26,10 +28,10 @@ export class CreateCommand {
constructor( constructor(
private cipherService: CipherService, private cipherService: CipherService,
private folderService: FolderService, private folderService: FolderService,
private stateService: StateService,
private cryptoService: CryptoService, private cryptoService: CryptoService,
private apiService: ApiService, private apiService: ApiService,
private folderApiService: FolderApiServiceAbstraction, private folderApiService: FolderApiServiceAbstraction,
private accountProfileService: BillingAccountProfileStateService,
) {} ) {}
async run( async run(
@ -124,7 +126,10 @@ export class CreateCommand {
return Response.notFound(); return Response.notFound();
} }
if (cipher.organizationId == null && !(await this.stateService.getCanAccessPremium())) { if (
cipher.organizationId == null &&
!(await firstValueFrom(this.accountProfileService.hasPremiumFromAnySource$))
) {
return Response.error("Premium status is required to use this feature."); return Response.error("Premium status is required to use this feature.");
} }

View File

@ -1,5 +1,7 @@
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
@ -12,9 +14,9 @@ export class DeleteCommand {
constructor( constructor(
private cipherService: CipherService, private cipherService: CipherService,
private folderService: FolderService, private folderService: FolderService,
private stateService: StateService,
private apiService: ApiService, private apiService: ApiService,
private folderApiService: FolderApiServiceAbstraction, private folderApiService: FolderApiServiceAbstraction,
private accountProfileService: BillingAccountProfileStateService,
) {} ) {}
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> { async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
@ -75,7 +77,10 @@ export class DeleteCommand {
return Response.error("Attachment `" + id + "` was not found."); return Response.error("Attachment `" + id + "` was not found.");
} }
if (cipher.organizationId == null && !(await this.stateService.getCanAccessPremium())) { const canAccessPremium = await firstValueFrom(
this.accountProfileService.hasPremiumFromAnySource$,
);
if (cipher.organizationId == null && !canAccessPremium) {
return Response.error("Premium status is required to use this feature."); return Response.error("Premium status is required to use this feature.");
} }

View File

@ -4,6 +4,7 @@ import { FormBuilder } from "@angular/forms";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -32,6 +33,7 @@ export class AddEditComponent extends BaseAddEditComponent {
sendApiService: SendApiService, sendApiService: SendApiService,
dialogService: DialogService, dialogService: DialogService,
formBuilder: FormBuilder, formBuilder: FormBuilder,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
i18nService, i18nService,
@ -46,6 +48,7 @@ export class AddEditComponent extends BaseAddEditComponent {
sendApiService, sendApiService,
dialogService, dialogService,
formBuilder, formBuilder,
billingAccountProfileStateService,
); );
} }

View File

@ -7,7 +7,7 @@
{{ "premiumMembership" | i18n }} {{ "premiumMembership" | i18n }}
</h1> </h1>
<div class="box-content box-content-padded"> <div class="box-content box-content-padded">
<div *ngIf="!isPremium"> <div *ngIf="!(isPremium$ | async)">
<p class="text-center lead">{{ "premiumNotCurrentMember" | i18n }}</p> <p class="text-center lead">{{ "premiumNotCurrentMember" | i18n }}</p>
<p>{{ "premiumSignUpAndGet" | i18n }}</p> <p>{{ "premiumSignUpAndGet" | i18n }}</p>
<ul class="bwi-ul"> <ul class="bwi-ul">
@ -40,7 +40,7 @@
{{ "premiumPrice" | i18n: (price | currency: "$") }} {{ "premiumPrice" | i18n: (price | currency: "$") }}
</p> </p>
</div> </div>
<div *ngIf="isPremium"> <div *ngIf="isPremium$ | async">
<p class="text-center lead">{{ "premiumCurrentMember" | i18n }}</p> <p class="text-center lead">{{ "premiumCurrentMember" | i18n }}</p>
<p class="text-center">{{ "premiumCurrentMemberThanks" | i18n }}</p> <p class="text-center">{{ "premiumCurrentMemberThanks" | i18n }}</p>
</div> </div>
@ -48,7 +48,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="primary" (click)="manage()" *ngIf="isPremium"> <button type="button" class="primary" (click)="manage()" *ngIf="isPremium$ | async">
<b>{{ "premiumManage" | i18n }}</b> <b>{{ "premiumManage" | i18n }}</b>
</button> </button>
<button <button
@ -56,13 +56,13 @@
type="button" type="button"
class="primary" class="primary"
(click)="purchase()" (click)="purchase()"
*ngIf="!isPremium" *ngIf="!(isPremium$ | async)"
[disabled]="$any(purchaseBtn).loading" [disabled]="$any(purchaseBtn).loading"
> >
<b>{{ "premiumPurchase" | i18n }}</b> <b>{{ "premiumPurchase" | i18n }}</b>
</button> </button>
<button type="button" data-dismiss="modal">{{ "close" | i18n }}</button> <button type="button" data-dismiss="modal">{{ "close" | i18n }}</button>
<div class="right" *ngIf="!isPremium"> <div class="right" *ngIf="!(isPremium$ | async)">
<button <button
#refreshBtn #refreshBtn
type="button" type="button"

View File

@ -2,6 +2,7 @@ import { Component } from "@angular/core";
import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/vault/components/premium.component"; import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/vault/components/premium.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -22,6 +23,7 @@ export class PremiumComponent extends BasePremiumComponent {
stateService: StateService, stateService: StateService,
dialogService: DialogService, dialogService: DialogService,
environmentService: EnvironmentService, environmentService: EnvironmentService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
i18nService, i18nService,
@ -31,6 +33,7 @@ export class PremiumComponent extends BasePremiumComponent {
stateService, stateService,
dialogService, dialogService,
environmentService, environmentService,
billingAccountProfileStateService,
); );
} }
} }

View File

@ -2,6 +2,7 @@ import { Component } from "@angular/core";
import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component"; import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -26,6 +27,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
stateService: StateService, stateService: StateService,
fileDownloadService: FileDownloadService, fileDownloadService: FileDownloadService,
dialogService: DialogService, dialogService: DialogService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
cipherService, cipherService,
@ -38,6 +40,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
stateService, stateService,
fileDownloadService, fileDownloadService,
dialogService, dialogService,
billingAccountProfileStateService,
); );
} }
} }

View File

@ -8,6 +8,7 @@ import {
ViewContainerRef, ViewContainerRef,
} from "@angular/core"; } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { first } from "rxjs/operators"; import { first } from "rxjs/operators";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
@ -15,6 +16,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service";
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -84,6 +86,7 @@ export class VaultComponent implements OnInit, OnDestroy {
activeFilter: VaultFilter = new VaultFilter(); activeFilter: VaultFilter = new VaultFilter();
private modal: ModalRef = null; private modal: ModalRef = null;
private componentIsDestroyed$ = new Subject<boolean>();
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@ -103,10 +106,16 @@ export class VaultComponent implements OnInit, OnDestroy {
private searchBarService: SearchBarService, private searchBarService: SearchBarService,
private apiService: ApiService, private apiService: ApiService,
private dialogService: DialogService, private dialogService: DialogService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
this.userHasPremiumAccess = await this.stateService.getCanAccessPremium(); this.billingAccountProfileStateService.hasPremiumFromAnySource$
.pipe(takeUntil(this.componentIsDestroyed$))
.subscribe((canAccessPremium: boolean) => {
this.userHasPremiumAccess = canAccessPremium;
});
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
@ -229,6 +238,8 @@ export class VaultComponent implements OnInit, OnDestroy {
ngOnDestroy() { ngOnDestroy() {
this.searchBarService.setEnabled(false); this.searchBarService.setEnabled(false);
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
this.componentIsDestroyed$.next(true);
this.componentIsDestroyed$.complete();
} }
async load() { async load() {

View File

@ -13,6 +13,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
@ -58,6 +59,7 @@ export class ViewComponent extends BaseViewComponent implements OnChanges {
fileDownloadService: FileDownloadService, fileDownloadService: FileDownloadService,
dialogService: DialogService, dialogService: DialogService,
datePipe: DatePipe, datePipe: DatePipe,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
cipherService, cipherService,
@ -80,6 +82,7 @@ export class ViewComponent extends BaseViewComponent implements OnChanges {
fileDownloadService, fileDownloadService,
dialogService, dialogService,
datePipe, datePipe,
billingAccountProfileStateService,
); );
} }
ngOnInit() { ngOnInit() {

View File

@ -8,8 +8,8 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component"; import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component";
import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component"; import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component";
@ -27,10 +27,16 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
messagingService: MessagingService, messagingService: MessagingService,
policyService: PolicyService, policyService: PolicyService,
private route: ActivatedRoute, private route: ActivatedRoute,
stateService: StateService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super(apiService, modalService, messagingService, policyService, stateService); super(
apiService,
modalService,
messagingService,
policyService,
billingAccountProfileStateService,
);
} }
async ngOnInit() { async ngOnInit() {

View File

@ -2,6 +2,7 @@ import { Component } from "@angular/core";
import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component"; import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -30,6 +31,7 @@ export class EmergencyAccessAttachmentsComponent extends BaseAttachmentsComponen
logService: LogService, logService: LogService,
fileDownloadService: FileDownloadService, fileDownloadService: FileDownloadService,
dialogService: DialogService, dialogService: DialogService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
cipherService, cipherService,
@ -42,6 +44,7 @@ export class EmergencyAccessAttachmentsComponent extends BaseAttachmentsComponen
stateService, stateService,
fileDownloadService, fileDownloadService,
dialogService, dialogService,
billingAccountProfileStateService,
); );
} }

View File

@ -28,7 +28,7 @@
bitButton bitButton
buttonType="primary" buttonType="primary"
[bitAction]="invite" [bitAction]="invite"
[disabled]="!canAccessPremium" [disabled]="!(canAccessPremium$ | async)"
> >
<i aria-hidden="true" class="bwi bwi-plus bwi-fw"></i> <i aria-hidden="true" class="bwi bwi-plus bwi-fw"></i>
{{ "addEmergencyContact" | i18n }} {{ "addEmergencyContact" | i18n }}

View File

@ -1,8 +1,9 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { lastValueFrom } from "rxjs"; import { lastValueFrom, Observable, firstValueFrom } from "rxjs";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@ -44,7 +45,7 @@ export class EmergencyAccessComponent implements OnInit {
confirmModalRef: ViewContainerRef; confirmModalRef: ViewContainerRef;
loaded = false; loaded = false;
canAccessPremium: boolean; canAccessPremium$: Observable<boolean>;
trustedContacts: GranteeEmergencyAccess[]; trustedContacts: GranteeEmergencyAccess[];
grantedContacts: GrantorEmergencyAccess[]; grantedContacts: GrantorEmergencyAccess[];
emergencyAccessType = EmergencyAccessType; emergencyAccessType = EmergencyAccessType;
@ -62,10 +63,12 @@ export class EmergencyAccessComponent implements OnInit {
private stateService: StateService, private stateService: StateService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
protected dialogService: DialogService, protected dialogService: DialogService,
) {} billingAccountProfileStateService: BillingAccountProfileStateService,
) {
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
}
async ngOnInit() { async ngOnInit() {
this.canAccessPremium = await this.stateService.getCanAccessPremium();
const orgs = await this.organizationService.getAll(); const orgs = await this.organizationService.getAll();
this.isOrganizationOwner = orgs.some((o) => o.isOwner); this.isOrganizationOwner = orgs.some((o) => o.isOwner);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
@ -80,18 +83,21 @@ export class EmergencyAccessComponent implements OnInit {
} }
async premiumRequired() { async premiumRequired() {
if (!this.canAccessPremium) { const canAccessPremium = await firstValueFrom(this.canAccessPremium$);
if (!canAccessPremium) {
this.messagingService.send("premiumRequired"); this.messagingService.send("premiumRequired");
return; return;
} }
} }
edit = async (details: GranteeEmergencyAccess) => { edit = async (details: GranteeEmergencyAccess) => {
const canAccessPremium = await firstValueFrom(this.canAccessPremium$);
const dialogRef = EmergencyAccessAddEditComponent.open(this.dialogService, { const dialogRef = EmergencyAccessAddEditComponent.open(this.dialogService, {
data: { data: {
name: this.userNamePipe.transform(details), name: this.userNamePipe.transform(details),
emergencyAccessId: details?.id, emergencyAccessId: details?.id,
readOnly: !this.canAccessPremium, readOnly: !canAccessPremium,
}, },
}); });

View File

@ -5,6 +5,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -52,6 +53,7 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent {
dialogService: DialogService, dialogService: DialogService,
datePipe: DatePipe, datePipe: DatePipe,
configService: ConfigServiceAbstraction, configService: ConfigServiceAbstraction,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
cipherService, cipherService,
@ -73,6 +75,7 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent {
dialogService, dialogService,
datePipe, datePipe,
configService, configService,
billingAccountProfileStateService,
); );
} }

View File

@ -70,7 +70,7 @@
type="button" type="button"
bitButton bitButton
buttonType="secondary" buttonType="secondary"
[disabled]="!canAccessPremium && p.premium" [disabled]="!(canAccessPremium$ | async) && p.premium"
(click)="manage(p.type)" (click)="manage(p.type)"
> >
{{ "manage" | i18n }} {{ "manage" | i18n }}

View File

@ -1,5 +1,5 @@
import { Component, OnDestroy, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core"; import { Component, OnDestroy, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core";
import { Subject, takeUntil } from "rxjs"; import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
@ -9,9 +9,9 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service"; import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ProductType } from "@bitwarden/common/enums"; import { ProductType } from "@bitwarden/common/enums";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { TwoFactorAuthenticatorComponent } from "./two-factor-authenticator.component"; import { TwoFactorAuthenticatorComponent } from "./two-factor-authenticator.component";
import { TwoFactorDuoComponent } from "./two-factor-duo.component"; import { TwoFactorDuoComponent } from "./two-factor-duo.component";
@ -40,7 +40,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
organizationId: string; organizationId: string;
organization: Organization; organization: Organization;
providers: any[] = []; providers: any[] = [];
canAccessPremium: boolean; canAccessPremium$: Observable<boolean>;
showPolicyWarning = false; showPolicyWarning = false;
loading = true; loading = true;
modal: ModalRef; modal: ModalRef;
@ -56,12 +56,12 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
protected modalService: ModalService, protected modalService: ModalService,
protected messagingService: MessagingService, protected messagingService: MessagingService,
protected policyService: PolicyService, protected policyService: PolicyService,
private stateService: StateService, billingAccountProfileStateService: BillingAccountProfileStateService,
) {} ) {
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
}
async ngOnInit() { async ngOnInit() {
this.canAccessPremium = await this.stateService.getCanAccessPremium();
for (const key in TwoFactorProviders) { for (const key in TwoFactorProviders) {
// eslint-disable-next-line // eslint-disable-next-line
if (!TwoFactorProviders.hasOwnProperty(key)) { if (!TwoFactorProviders.hasOwnProperty(key)) {
@ -174,7 +174,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
} }
async premiumRequired() { async premiumRequired() {
if (!this.canAccessPremium) { if (!(await firstValueFrom(this.canAccessPremium$))) {
this.messagingService.send("premiumRequired"); this.messagingService.send("premiumRequired");
return; return;
} }

View File

@ -6,7 +6,7 @@
</div> </div>
<bit-callout <bit-callout
type="info" type="info"
*ngIf="canAccessPremium" *ngIf="canAccessPremium$ | async"
title="{{ 'youHavePremiumAccess' | i18n }}" title="{{ 'youHavePremiumAccess' | i18n }}"
icon="bwi bwi-star-f" icon="bwi bwi-star-f"
> >

View File

@ -1,14 +1,15 @@
import { Component, OnInit, ViewChild } from "@angular/core"; import { Component, OnInit, ViewChild } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { firstValueFrom, Observable } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { PaymentComponent, TaxInfoComponent } from "../shared"; import { PaymentComponent, TaxInfoComponent } from "../shared";
@ -20,7 +21,7 @@ export class PremiumComponent implements OnInit {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent; @ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
canAccessPremium = false; canAccessPremium$: Observable<boolean>;
selfHosted = false; selfHosted = false;
premiumPrice = 10; premiumPrice = 10;
familyPlanMaxUserCount = 6; familyPlanMaxUserCount = 6;
@ -39,17 +40,16 @@ export class PremiumComponent implements OnInit {
private messagingService: MessagingService, private messagingService: MessagingService,
private syncService: SyncService, private syncService: SyncService,
private logService: LogService, private logService: LogService,
private stateService: StateService,
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
this.selfHosted = platformUtilsService.isSelfHost(); this.selfHosted = platformUtilsService.isSelfHost();
this.cloudWebVaultUrl = this.environmentService.getCloudWebVaultUrl(); this.cloudWebVaultUrl = this.environmentService.getCloudWebVaultUrl();
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
} }
async ngOnInit() { async ngOnInit() {
this.canAccessPremium = await this.stateService.getCanAccessPremium(); if (await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$)) {
const premiumPersonally = await this.stateService.getHasPremiumPersonally();
if (premiumPersonally) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/settings/subscription/user-subscription"]); this.router.navigate(["/settings/subscription/user-subscription"]);

View File

@ -1,6 +1,8 @@
<app-header> <app-header>
<bit-tab-nav-bar slot="tabs" *ngIf="!selfHosted"> <bit-tab-nav-bar slot="tabs" *ngIf="!selfHosted">
<bit-tab-link [route]="subscriptionRoute">{{ "subscription" | i18n }}</bit-tab-link> <bit-tab-link [route]="(hasPremium$ | async) ? 'user-subscription' : 'premium'">{{
"subscription" | i18n
}}</bit-tab-link>
<bit-tab-link route="payment-method">{{ "paymentMethod" | i18n }}</bit-tab-link> <bit-tab-link route="payment-method">{{ "paymentMethod" | i18n }}</bit-tab-link>
<bit-tab-link route="billing-history">{{ "billingHistory" | i18n }}</bit-tab-link> <bit-tab-link route="billing-history">{{ "billingHistory" | i18n }}</bit-tab-link>
</bit-tab-nav-bar> </bit-tab-nav-bar>

View File

@ -1,26 +1,24 @@
import { Component } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { Observable } from "rxjs";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
@Component({ @Component({
templateUrl: "subscription.component.html", templateUrl: "subscription.component.html",
}) })
export class SubscriptionComponent { export class SubscriptionComponent implements OnInit {
hasPremium: boolean; hasPremium$: Observable<boolean>;
selfHosted: boolean; selfHosted: boolean;
constructor( constructor(
private stateService: StateService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
) {} billingAccountProfileStateService: BillingAccountProfileStateService,
) {
this.hasPremium$ = billingAccountProfileStateService.hasPremiumPersonally$;
}
async ngOnInit() { ngOnInit() {
this.hasPremium = await this.stateService.getHasPremiumPersonally();
this.selfHosted = this.platformUtilsService.isSelfHost(); this.selfHosted = this.platformUtilsService.isSelfHost();
} }
get subscriptionRoute(): string {
return this.hasPremium ? "user-subscription" : "premium";
}
} }

View File

@ -1,8 +1,9 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { lastValueFrom, Observable } from "rxjs"; import { firstValueFrom, lastValueFrom, Observable } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response"; import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction as ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { ConfigServiceAbstraction as ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
@ -11,7 +12,6 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { import {
@ -37,7 +37,6 @@ export class UserSubscriptionComponent implements OnInit {
presentUserWithOffboardingSurvey$: Observable<boolean>; presentUserWithOffboardingSurvey$: Observable<boolean>;
constructor( constructor(
private stateService: StateService,
private apiService: ApiService, private apiService: ApiService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private i18nService: I18nService,
@ -47,6 +46,7 @@ export class UserSubscriptionComponent implements OnInit {
private dialogService: DialogService, private dialogService: DialogService,
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private configService: ConfigService, private configService: ConfigService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
this.selfHosted = platformUtilsService.isSelfHost(); this.selfHosted = platformUtilsService.isSelfHost();
this.cloudWebVaultUrl = this.environmentService.getCloudWebVaultUrl(); this.cloudWebVaultUrl = this.environmentService.getCloudWebVaultUrl();
@ -65,8 +65,7 @@ export class UserSubscriptionComponent implements OnInit {
return; return;
} }
// eslint-disable-next-line @typescript-eslint/no-misused-promises if (await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$)) {
if (this.stateService.getHasPremiumPersonally()) {
this.loading = true; this.loading = true;
this.sub = await this.apiService.getUserSubscription(); this.sub = await this.apiService.getUserSubscription();
} else { } else {

View File

@ -4,32 +4,39 @@ import {
RouterStateSnapshot, RouterStateSnapshot,
Router, Router,
CanActivateFn, CanActivateFn,
UrlTree,
} from "@angular/router"; } from "@angular/router";
import { Observable } from "rxjs";
import { tap } from "rxjs/operators";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
/** /**
* CanActivate guard that checks if the user has premium and otherwise triggers the "premiumRequired" * CanActivate guard that checks if the user has premium and otherwise triggers the "premiumRequired"
* message and blocks navigation. * message and blocks navigation.
*/ */
export function hasPremiumGuard(): CanActivateFn { export function hasPremiumGuard(): CanActivateFn {
return async (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => { return (
_route: ActivatedRouteSnapshot,
_state: RouterStateSnapshot,
): Observable<boolean | UrlTree> => {
const router = inject(Router); const router = inject(Router);
const stateService = inject(StateService);
const messagingService = inject(MessagingService); const messagingService = inject(MessagingService);
const billingAccountProfileStateService = inject(BillingAccountProfileStateService);
const userHasPremium = await stateService.getCanAccessPremium(); return billingAccountProfileStateService.hasPremiumFromAnySource$.pipe(
tap((userHasPremium: boolean) => {
if (!userHasPremium) { if (!userHasPremium) {
messagingService.send("premiumRequired"); messagingService.send("premiumRequired");
} }
}),
// Prevent trapping the user on the login page, since that's an awful UX flow // Prevent trapping the user on the login page, since that's an awful UX flow
if (!userHasPremium && router.url === "/login") { tap((userHasPremium: boolean) => {
return router.createUrlTree(["/"]); if (!userHasPremium && router.url === "/login") {
} return router.createUrlTree(["/"]);
}
return userHasPremium; }),
);
}; };
} }

View File

@ -1,15 +1,16 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { RouterModule } from "@angular/router"; import { RouterModule } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigServiceAbstraction as ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { ConfigServiceAbstraction as ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components"; import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components";
@ -48,10 +49,10 @@ export class UserLayoutComponent implements OnInit, OnDestroy {
private ngZone: NgZone, private ngZone: NgZone,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
private stateService: StateService,
private apiService: ApiService, private apiService: ApiService,
private syncService: SyncService, private syncService: SyncService,
private configService: ConfigService, private configService: ConfigService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@ -79,16 +80,21 @@ export class UserLayoutComponent implements OnInit, OnDestroy {
} }
async load() { async load() {
const premium = await this.stateService.getHasPremiumPersonally(); const hasPremiumPersonally = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumPersonally$,
);
const hasPremiumFromOrg = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$,
);
const selfHosted = this.platformUtilsService.isSelfHost(); const selfHosted = this.platformUtilsService.isSelfHost();
this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships(); this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships();
const hasPremiumFromOrg = await this.stateService.getHasPremiumFromOrganization();
let billing = null; let billing = null;
if (!selfHosted) { if (!selfHosted) {
// TODO: We should remove the need to call this! // TODO: We should remove the need to call this!
billing = await this.apiService.getUserBillingHistory(); billing = await this.apiService.getUserBillingHistory();
} }
this.hideSubscription = !premium && hasPremiumFromOrg && (selfHosted || billing?.hasNoHistory); this.hideSubscription =
!hasPremiumPersonally && hasPremiumFromOrg && (selfHosted || billing?.hasNoHistory);
} }
} }

View File

@ -1,12 +1,12 @@
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "../core";
const BroadcasterSubscriptionId = "SettingsComponent"; const BroadcasterSubscriptionId = "SettingsComponent";
@Component({ @Component({
@ -24,8 +24,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
private ngZone: NgZone, private ngZone: NgZone,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
private stateService: StateService,
private apiService: ApiService, private apiService: ApiService,
private billingAccountProfileStateServiceAbstraction: BillingAccountProfileStateService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@ -51,9 +51,13 @@ export class SettingsComponent implements OnInit, OnDestroy {
} }
async load() { async load() {
this.premium = await this.stateService.getHasPremiumPersonally(); this.premium = await firstValueFrom(
this.billingAccountProfileStateServiceAbstraction.hasPremiumPersonally$,
);
this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships(); this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships();
const hasPremiumFromOrg = await this.stateService.getHasPremiumFromOrganization(); const hasPremiumFromOrg = await firstValueFrom(
this.billingAccountProfileStateServiceAbstraction.hasPremiumFromAnyOrganization$,
);
let billing = null; let billing = null;
if (!this.selfHosted) { if (!this.selfHosted) {
billing = await this.apiService.getUserBillingHistory(); billing = await this.apiService.getUserBillingHistory();

View File

@ -1,6 +1,7 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { reports, ReportType } from "../reports"; import { reports, ReportType } from "../reports";
import { ReportEntry, ReportVariant } from "../shared"; import { ReportEntry, ReportVariant } from "../shared";
@ -12,11 +13,12 @@ import { ReportEntry, ReportVariant } from "../shared";
export class ReportsHomeComponent implements OnInit { export class ReportsHomeComponent implements OnInit {
reports: ReportEntry[]; reports: ReportEntry[];
constructor(private stateService: StateService) {} constructor(private billingAccountProfileStateService: BillingAccountProfileStateService) {}
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
const userHasPremium = await this.stateService.getCanAccessPremium(); const userHasPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
);
const reportRequiresPremium = userHasPremium const reportRequiresPremium = userHasPremium
? ReportVariant.Enabled ? ReportVariant.Enabled
: ReportVariant.RequiresPremium; : ReportVariant.RequiresPremium;

View File

@ -5,6 +5,7 @@ import { FormBuilder } from "@angular/forms";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -36,6 +37,7 @@ export class AddEditComponent extends BaseAddEditComponent {
sendApiService: SendApiService, sendApiService: SendApiService,
dialogService: DialogService, dialogService: DialogService,
formBuilder: FormBuilder, formBuilder: FormBuilder,
billingAccountProfileStateService: BillingAccountProfileStateService,
protected dialogRef: DialogRef, protected dialogRef: DialogRef,
@Inject(DIALOG_DATA) params: { sendId: string }, @Inject(DIALOG_DATA) params: { sendId: string },
) { ) {
@ -52,6 +54,7 @@ export class AddEditComponent extends BaseAddEditComponent {
sendApiService, sendApiService,
dialogService, dialogService,
formBuilder, formBuilder,
billingAccountProfileStateService,
); );
this.sendId = params.sendId; this.sendId = params.sendId;

View File

@ -1,22 +1,33 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
@Component({ @Component({
selector: "app-tools", selector: "app-tools",
templateUrl: "tools.component.html", templateUrl: "tools.component.html",
}) })
export class ToolsComponent implements OnInit { export class ToolsComponent implements OnInit, OnDestroy {
private componentIsDestroyed$ = new Subject<boolean>();
canAccessPremium = false; canAccessPremium = false;
constructor( constructor(
private stateService: StateService,
private messagingService: MessagingService, private messagingService: MessagingService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
this.canAccessPremium = await this.stateService.getCanAccessPremium(); this.billingAccountProfileStateService.hasPremiumFromAnySource$
.pipe(takeUntil(this.componentIsDestroyed$))
.subscribe((canAccessPremium: boolean) => {
this.canAccessPremium = canAccessPremium;
});
}
ngOnDestroy() {
this.componentIsDestroyed$.next(true);
this.componentIsDestroyed$.complete();
} }
premiumRequired() { premiumRequired() {

View File

@ -3,8 +3,6 @@ import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
import { BadgeModule, I18nMockService } from "@bitwarden/components"; import { BadgeModule, I18nMockService } from "@bitwarden/components";
import { PremiumBadgeComponent } from "./premium-badge.component"; import { PremiumBadgeComponent } from "./premium-badge.component";
@ -15,12 +13,6 @@ class MockMessagingService implements MessagingService {
} }
} }
class MockedStateService implements Partial<StateService> {
async getCanAccessPremium(options?: StorageOptions) {
return false;
}
}
export default { export default {
title: "Web/Premium Badge", title: "Web/Premium Badge",
component: PremiumBadgeComponent, component: PremiumBadgeComponent,
@ -42,12 +34,6 @@ export default {
return new MockMessagingService(); return new MockMessagingService();
}, },
}, },
{
provide: StateService,
useFactory: () => {
return new MockedStateService();
},
},
], ],
}), }),
], ],

View File

@ -1,11 +1,13 @@
import { DatePipe } from "@angular/common"; import { DatePipe } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType, ProductType } from "@bitwarden/common/enums"; import { EventType, ProductType } from "@bitwarden/common/enums";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -64,6 +66,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
dialogService: DialogService, dialogService: DialogService,
datePipe: DatePipe, datePipe: DatePipe,
configService: ConfigServiceAbstraction, configService: ConfigServiceAbstraction,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
cipherService, cipherService,
@ -98,7 +101,9 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
this.hasPasswordHistory = this.cipher.hasPasswordHistory; this.hasPasswordHistory = this.cipher.hasPasswordHistory;
this.cleanUp(); this.cleanUp();
this.canAccessPremium = await this.stateService.getCanAccessPremium(); this.canAccessPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
);
if (this.showTotp()) { if (this.showTotp()) {
await this.totpUpdateCode(); await this.totpUpdateCode();
const interval = this.totpService.getTimeInterval(this.cipher.login.totp); const interval = this.totpService.getTimeInterval(this.cipher.login.totp);

View File

@ -2,6 +2,7 @@ import { Component } from "@angular/core";
import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component"; import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -30,6 +31,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
logService: LogService, logService: LogService,
fileDownloadService: FileDownloadService, fileDownloadService: FileDownloadService,
dialogService: DialogService, dialogService: DialogService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
cipherService, cipherService,
@ -42,6 +44,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
stateService, stateService,
fileDownloadService, fileDownloadService,
dialogService, dialogService,
billingAccountProfileStateService,
); );
} }

View File

@ -37,6 +37,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
@ -182,6 +183,7 @@ export class VaultComponent implements OnInit, OnDestroy {
private configService: ConfigServiceAbstraction, private configService: ConfigServiceAbstraction,
private apiService: ApiService, private apiService: ApiService,
private userVerificationService: UserVerificationService, private userVerificationService: UserVerificationService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@ -201,7 +203,9 @@ export class VaultComponent implements OnInit, OnDestroy {
: false; : false;
await this.syncService.fullSync(false); await this.syncService.fullSync(false);
const canAccessPremium = await this.stateService.getCanAccessPremium(); const canAccessPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
);
this.showPremiumCallout = this.showPremiumCallout =
!this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost(); !this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost();
@ -242,9 +246,6 @@ export class VaultComponent implements OnInit, OnDestroy {
}); });
const filter$ = this.routedVaultFilterService.filter$; const filter$ = this.routedVaultFilterService.filter$;
const canAccessPremium$ = Utils.asyncToObservable(() =>
this.stateService.getCanAccessPremium(),
).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
const allCollections$ = Utils.asyncToObservable(() => this.collectionService.getAllDecrypted()); const allCollections$ = Utils.asyncToObservable(() => this.collectionService.getAllDecrypted());
const nestedCollections$ = allCollections$.pipe( const nestedCollections$ = allCollections$.pipe(
map((collections) => getNestedCollectionTree(collections)), map((collections) => getNestedCollectionTree(collections)),
@ -368,7 +369,7 @@ export class VaultComponent implements OnInit, OnDestroy {
switchMap(() => switchMap(() =>
combineLatest([ combineLatest([
filter$, filter$,
canAccessPremium$, this.billingAccountProfileStateService.hasPremiumFromAnySource$,
allCollections$, allCollections$,
this.organizationService.organizations$, this.organizationService.organizations$,
ciphers$, ciphers$,
@ -513,8 +514,7 @@ export class VaultComponent implements OnInit, OnDestroy {
return; return;
} }
const canAccessPremium = await this.stateService.getCanAccessPremium(); if (cipher.organizationId == null && !this.canAccessPremium) {
if (cipher.organizationId == null && !canAccessPremium) {
this.messagingService.send("premiumRequired"); this.messagingService.send("premiumRequired");
return; return;
} else if (cipher.organizationId != null) { } else if (cipher.organizationId != null) {

View File

@ -6,6 +6,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -54,6 +55,7 @@ export class AddEditComponent extends BaseAddEditComponent {
dialogService: DialogService, dialogService: DialogService,
datePipe: DatePipe, datePipe: DatePipe,
configService: ConfigServiceAbstraction, configService: ConfigServiceAbstraction,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
cipherService, cipherService,
@ -75,6 +77,7 @@ export class AddEditComponent extends BaseAddEditComponent {
dialogService, dialogService,
datePipe, datePipe,
configService, configService,
billingAccountProfileStateService,
); );
} }

View File

@ -2,6 +2,7 @@ import { Component } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -34,6 +35,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
logService: LogService, logService: LogService,
fileDownloadService: FileDownloadService, fileDownloadService: FileDownloadService,
dialogService: DialogService, dialogService: DialogService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
cipherService, cipherService,
@ -45,6 +47,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
logService, logService,
fileDownloadService, fileDownloadService,
dialogService, dialogService,
billingAccountProfileStateService,
); );
} }

View File

@ -1,6 +1,7 @@
import { Directive, OnInit, TemplateRef, ViewContainerRef } from "@angular/core"; import { Directive, OnInit, TemplateRef, ViewContainerRef } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
/** /**
* Hides the element if the user has premium. * Hides the element if the user has premium.
@ -12,11 +13,13 @@ export class NotPremiumDirective implements OnInit {
constructor( constructor(
private templateRef: TemplateRef<any>, private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef, private viewContainer: ViewContainerRef,
private stateService: StateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
) {} ) {}
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
const premium = await this.stateService.getCanAccessPremium(); const premium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
);
if (premium) { if (premium) {
this.viewContainer.clear(); this.viewContainer.clear();

View File

@ -1,6 +1,7 @@
import { Directive, OnInit, TemplateRef, ViewContainerRef } from "@angular/core"; import { Directive, OnDestroy, OnInit, TemplateRef, ViewContainerRef } from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
/** /**
* Only shows the element if the user has premium. * Only shows the element if the user has premium.
@ -8,20 +9,29 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
@Directive({ @Directive({
selector: "[appPremium]", selector: "[appPremium]",
}) })
export class PremiumDirective implements OnInit { export class PremiumDirective implements OnInit, OnDestroy {
private directiveIsDestroyed$ = new Subject<boolean>();
constructor( constructor(
private templateRef: TemplateRef<any>, private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef, private viewContainer: ViewContainerRef,
private stateService: StateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
) {} ) {}
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
const premium = await this.stateService.getCanAccessPremium(); this.billingAccountProfileStateService.hasPremiumFromAnySource$
.pipe(takeUntil(this.directiveIsDestroyed$))
.subscribe((premium: boolean) => {
if (premium) {
this.viewContainer.clear();
} else {
this.viewContainer.createEmbeddedView(this.templateRef);
}
});
}
if (premium) { ngOnDestroy() {
this.viewContainer.createEmbeddedView(this.templateRef); this.directiveIsDestroyed$.next(true);
} else { this.directiveIsDestroyed$.complete();
this.viewContainer.clear();
}
} }
} }

View File

@ -96,9 +96,11 @@ import {
DomainSettingsService, DomainSettingsService,
DefaultDomainSettingsService, DefaultDomainSettingsService,
} from "@bitwarden/common/autofill/services/domain-settings.service"; } from "@bitwarden/common/autofill/services/domain-settings.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-billing.service"; import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-billing.service";
import { PaymentMethodWarningsServiceAbstraction } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; import { PaymentMethodWarningsServiceAbstraction } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service";
import { BillingApiService } from "@bitwarden/common/billing/services/billing-api.service"; import { BillingApiService } from "@bitwarden/common/billing/services/billing-api.service";
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
import { PaymentMethodWarningsService } from "@bitwarden/common/billing/services/payment-method-warnings.service"; import { PaymentMethodWarningsService } from "@bitwarden/common/billing/services/payment-method-warnings.service";
@ -368,6 +370,7 @@ const typesafeProviders: Array<SafeProvider> = [
DeviceTrustCryptoServiceAbstraction, DeviceTrustCryptoServiceAbstraction,
AuthRequestServiceAbstraction, AuthRequestServiceAbstraction,
GlobalStateProvider, GlobalStateProvider,
BillingAccountProfileStateService,
], ],
}), }),
safeProvider({ safeProvider({
@ -576,6 +579,7 @@ const typesafeProviders: Array<SafeProvider> = [
SendApiServiceAbstraction, SendApiServiceAbstraction,
AvatarServiceAbstraction, AvatarServiceAbstraction,
LOGOUT_CALLBACK, LOGOUT_CALLBACK,
BillingAccountProfileStateService,
], ],
}), }),
safeProvider({ provide: BroadcasterServiceAbstraction, useClass: BroadcasterService, deps: [] }), safeProvider({ provide: BroadcasterServiceAbstraction, useClass: BroadcasterService, deps: [] }),
@ -1045,6 +1049,11 @@ const typesafeProviders: Array<SafeProvider> = [
useClass: PaymentMethodWarningsService, useClass: PaymentMethodWarningsService,
deps: [BillingApiServiceAbstraction, StateProvider], deps: [BillingApiServiceAbstraction, StateProvider],
}), }),
safeProvider({
provide: BillingAccountProfileStateService,
useClass: DefaultBillingAccountProfileStateService,
deps: [ActiveUserStateProvider],
}),
]; ];
function encryptServiceFactory( function encryptServiceFactory(

View File

@ -5,6 +5,7 @@ import { BehaviorSubject, Subject, concatMap, firstValueFrom, map, takeUntil } f
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -116,6 +117,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
protected sendApiService: SendApiService, protected sendApiService: SendApiService,
protected dialogService: DialogService, protected dialogService: DialogService,
protected formBuilder: FormBuilder, protected formBuilder: FormBuilder,
protected billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
this.typeOptions = [ this.typeOptions = [
{ name: i18nService.t("sendTypeFile"), value: SendType.File, premium: true }, { name: i18nService.t("sendTypeFile"), value: SendType.File, premium: true },
@ -188,6 +190,12 @@ export class AddEditComponent implements OnInit, OnDestroy {
} }
}); });
this.billingAccountProfileStateService.hasPremiumFromAnySource$
.pipe(takeUntil(this.destroy$))
.subscribe((hasPremiumFromAnySource) => {
this.canAccessPremium = hasPremiumFromAnySource;
});
await this.load(); await this.load();
} }
@ -205,7 +213,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
} }
async load() { async load() {
this.canAccessPremium = await this.stateService.getCanAccessPremium();
this.emailVerified = await this.stateService.getEmailVerified(); this.emailVerified = await this.stateService.getEmailVerified();
this.type = !this.canAccessPremium || !this.emailVerified ? SendType.Text : SendType.File; this.type = !this.canAccessPremium || !this.emailVerified ? SendType.Text : SendType.File;

View File

@ -1,6 +1,8 @@
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
@ -42,6 +44,7 @@ export class AttachmentsComponent implements OnInit {
protected stateService: StateService, protected stateService: StateService,
protected fileDownloadService: FileDownloadService, protected fileDownloadService: FileDownloadService,
protected dialogService: DialogService, protected dialogService: DialogService,
protected billingAccountProfileStateService: BillingAccountProfileStateService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@ -185,7 +188,9 @@ export class AttachmentsComponent implements OnInit {
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain), await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain),
); );
const canAccessPremium = await this.stateService.getCanAccessPremium(); const canAccessPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
);
this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null; this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null;
if (!this.canAccessAttachments) { if (!this.canAccessAttachments) {

View File

@ -1,6 +1,8 @@
import { Directive, OnInit } from "@angular/core"; import { Directive } from "@angular/core";
import { Observable, Subject } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -9,11 +11,12 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
@Directive() @Directive()
export class PremiumComponent implements OnInit { export class PremiumComponent {
isPremium = false; isPremium$: Observable<boolean>;
price = 10; price = 10;
refreshPromise: Promise<any>; refreshPromise: Promise<any>;
cloudWebVaultUrl: string; cloudWebVaultUrl: string;
private directiveIsDestroyed$ = new Subject<boolean>();
constructor( constructor(
protected i18nService: I18nService, protected i18nService: I18nService,
@ -22,13 +25,11 @@ export class PremiumComponent implements OnInit {
private logService: LogService, private logService: LogService,
protected stateService: StateService, protected stateService: StateService,
protected dialogService: DialogService, protected dialogService: DialogService,
private environmentService: EnvironmentService, environmentService: EnvironmentService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
this.cloudWebVaultUrl = this.environmentService.getCloudWebVaultUrl(); this.cloudWebVaultUrl = environmentService.getCloudWebVaultUrl();
} this.isPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
async ngOnInit() {
this.isPremium = await this.stateService.getCanAccessPremium();
} }
async refresh() { async refresh() {
@ -36,7 +37,6 @@ export class PremiumComponent implements OnInit {
this.refreshPromise = this.apiService.refreshIdentityToken(); this.refreshPromise = this.apiService.refreshIdentityToken();
await this.refreshPromise; await this.refreshPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("refreshComplete")); this.platformUtilsService.showToast("success", null, this.i18nService.t("refreshComplete"));
this.isPremium = await this.stateService.getCanAccessPremium();
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
} }

View File

@ -9,12 +9,13 @@ import {
OnInit, OnInit,
Output, Output,
} from "@angular/core"; } from "@angular/core";
import { firstValueFrom } from "rxjs"; import { firstValueFrom, Subject, takeUntil } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
@ -68,6 +69,7 @@ export class ViewComponent implements OnDestroy, OnInit {
private totpInterval: any; private totpInterval: any;
private previousCipherId: string; private previousCipherId: string;
private passwordReprompted = false; private passwordReprompted = false;
private directiveIsDestroyed$ = new Subject<boolean>();
get fido2CredentialCreationDateValue(): string { get fido2CredentialCreationDateValue(): string {
const dateCreated = this.i18nService.t("dateCreated"); const dateCreated = this.i18nService.t("dateCreated");
@ -99,6 +101,7 @@ export class ViewComponent implements OnDestroy, OnInit {
protected fileDownloadService: FileDownloadService, protected fileDownloadService: FileDownloadService,
protected dialogService: DialogService, protected dialogService: DialogService,
protected datePipe: DatePipe, protected datePipe: DatePipe,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) {} ) {}
ngOnInit() { ngOnInit() {
@ -116,11 +119,19 @@ export class ViewComponent implements OnDestroy, OnInit {
} }
}); });
}); });
this.billingAccountProfileStateService.hasPremiumFromAnySource$
.pipe(takeUntil(this.directiveIsDestroyed$))
.subscribe((canAccessPremium: boolean) => {
this.canAccessPremium = canAccessPremium;
});
} }
ngOnDestroy() { ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
this.cleanUp(); this.cleanUp();
this.directiveIsDestroyed$.next(true);
this.directiveIsDestroyed$.complete();
} }
async load() { async load() {
@ -130,7 +141,6 @@ export class ViewComponent implements OnDestroy, OnInit {
this.cipher = await cipher.decrypt( this.cipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher), await this.cipherService.getKeyForCipherKeyDecryption(cipher),
); );
this.canAccessPremium = await this.stateService.getCanAccessPremium();
this.showPremiumRequiredTotp = this.showPremiumRequiredTotp =
this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp; this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp;

View File

@ -5,6 +5,7 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -37,6 +38,7 @@ describe("AuthRequestLoginStrategy", () => {
let stateService: MockProxy<StateService>; let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>; let twoFactorService: MockProxy<TwoFactorService>;
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>; let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let authRequestLoginStrategy: AuthRequestLoginStrategy; let authRequestLoginStrategy: AuthRequestLoginStrategy;
let credentials: AuthRequestLoginCredentials; let credentials: AuthRequestLoginCredentials;
@ -64,6 +66,7 @@ describe("AuthRequestLoginStrategy", () => {
stateService = mock<StateService>(); stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>(); twoFactorService = mock<TwoFactorService>();
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>(); deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
tokenService.getTwoFactorToken.mockResolvedValue(null); tokenService.getTwoFactorToken.mockResolvedValue(null);
appIdService.getAppId.mockResolvedValue(deviceId); appIdService.getAppId.mockResolvedValue(deviceId);
@ -81,6 +84,7 @@ describe("AuthRequestLoginStrategy", () => {
stateService, stateService,
twoFactorService, twoFactorService,
deviceTrustCryptoService, deviceTrustCryptoService,
billingAccountProfileStateService,
); );
tokenResponse = identityTokenResponseFactory(); tokenResponse = identityTokenResponseFactory();

View File

@ -9,6 +9,7 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request"; import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -54,6 +55,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
stateService: StateService, stateService: StateService,
twoFactorService: TwoFactorService, twoFactorService: TwoFactorService,
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
cryptoService, cryptoService,
@ -65,6 +67,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
logService, logService,
stateService, stateService,
twoFactorService, twoFactorService,
billingAccountProfileStateService,
); );
this.cache = new BehaviorSubject(data); this.cache = new BehaviorSubject(data);

View File

@ -14,6 +14,7 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response";
import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response"; import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -109,6 +110,7 @@ describe("LoginStrategy", () => {
let twoFactorService: MockProxy<TwoFactorService>; let twoFactorService: MockProxy<TwoFactorService>;
let policyService: MockProxy<PolicyService>; let policyService: MockProxy<PolicyService>;
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>; let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let passwordLoginStrategy: PasswordLoginStrategy; let passwordLoginStrategy: PasswordLoginStrategy;
let credentials: PasswordLoginCredentials; let credentials: PasswordLoginCredentials;
@ -127,6 +129,7 @@ describe("LoginStrategy", () => {
policyService = mock<PolicyService>(); policyService = mock<PolicyService>();
passwordStrengthService = mock<PasswordStrengthService>(); passwordStrengthService = mock<PasswordStrengthService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
appIdService.getAppId.mockResolvedValue(deviceId); appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeAccessToken.calledWith(accessToken).mockResolvedValue(decodedToken); tokenService.decodeAccessToken.calledWith(accessToken).mockResolvedValue(decodedToken);
@ -146,6 +149,7 @@ describe("LoginStrategy", () => {
passwordStrengthService, passwordStrengthService,
policyService, policyService,
loginStrategyService, loginStrategyService,
billingAccountProfileStateService,
); );
credentials = new PasswordLoginCredentials(email, masterPassword); credentials = new PasswordLoginCredentials(email, masterPassword);
}); });
@ -192,7 +196,6 @@ describe("LoginStrategy", () => {
userId: userId, userId: userId,
name: name, name: name,
email: email, email: email,
hasPremiumPersonally: false,
kdfIterations: kdfIterations, kdfIterations: kdfIterations,
kdfType: kdf, kdfType: kdf,
}, },
@ -409,6 +412,7 @@ describe("LoginStrategy", () => {
passwordStrengthService, passwordStrengthService,
policyService, policyService,
loginStrategyService, loginStrategyService,
billingAccountProfileStateService,
); );
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory()); apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());

View File

@ -15,6 +15,7 @@ import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request
import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response"; import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ClientType } from "@bitwarden/common/enums"; import { ClientType } from "@bitwarden/common/enums";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
@ -68,6 +69,7 @@ export abstract class LoginStrategy {
protected logService: LogService, protected logService: LogService,
protected stateService: StateService, protected stateService: StateService,
protected twoFactorService: TwoFactorService, protected twoFactorService: TwoFactorService,
protected billingAccountProfileStateService: BillingAccountProfileStateService,
) {} ) {}
abstract exportCache(): CacheData; abstract exportCache(): CacheData;
@ -191,7 +193,6 @@ export abstract class LoginStrategy {
userId, userId,
name: accountInformation.name, name: accountInformation.name,
email: accountInformation.email, email: accountInformation.email,
hasPremiumPersonally: accountInformation.premium,
kdfIterations: tokenResponse.kdfIterations, kdfIterations: tokenResponse.kdfIterations,
kdfMemory: tokenResponse.kdfMemory, kdfMemory: tokenResponse.kdfMemory,
kdfParallelism: tokenResponse.kdfParallelism, kdfParallelism: tokenResponse.kdfParallelism,
@ -206,6 +207,8 @@ export abstract class LoginStrategy {
adminAuthRequest: adminAuthRequest?.toJSON(), adminAuthRequest: adminAuthRequest?.toJSON(),
}), }),
); );
await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false);
} }
protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> { protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> {

View File

@ -9,6 +9,7 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -61,6 +62,7 @@ describe("PasswordLoginStrategy", () => {
let twoFactorService: MockProxy<TwoFactorService>; let twoFactorService: MockProxy<TwoFactorService>;
let policyService: MockProxy<PolicyService>; let policyService: MockProxy<PolicyService>;
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>; let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let passwordLoginStrategy: PasswordLoginStrategy; let passwordLoginStrategy: PasswordLoginStrategy;
let credentials: PasswordLoginCredentials; let credentials: PasswordLoginCredentials;
@ -79,6 +81,7 @@ describe("PasswordLoginStrategy", () => {
twoFactorService = mock<TwoFactorService>(); twoFactorService = mock<TwoFactorService>();
policyService = mock<PolicyService>(); policyService = mock<PolicyService>();
passwordStrengthService = mock<PasswordStrengthService>(); passwordStrengthService = mock<PasswordStrengthService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
appIdService.getAppId.mockResolvedValue(deviceId); appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeAccessToken.mockResolvedValue({}); tokenService.decodeAccessToken.mockResolvedValue({});
@ -108,6 +111,7 @@ describe("PasswordLoginStrategy", () => {
passwordStrengthService, passwordStrengthService,
policyService, policyService,
loginStrategyService, loginStrategyService,
billingAccountProfileStateService,
); );
credentials = new PasswordLoginCredentials(email, masterPassword); credentials = new PasswordLoginCredentials(email, masterPassword);
tokenResponse = identityTokenResponseFactory(masterPasswordPolicy); tokenResponse = identityTokenResponseFactory(masterPasswordPolicy);

View File

@ -13,6 +13,7 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide
import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response"; import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -86,6 +87,7 @@ export class PasswordLoginStrategy extends LoginStrategy {
private passwordStrengthService: PasswordStrengthServiceAbstraction, private passwordStrengthService: PasswordStrengthServiceAbstraction,
private policyService: PolicyService, private policyService: PolicyService,
private loginStrategyService: LoginStrategyServiceAbstraction, private loginStrategyService: LoginStrategyServiceAbstraction,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
cryptoService, cryptoService,
@ -97,6 +99,7 @@ export class PasswordLoginStrategy extends LoginStrategy {
logService, logService,
stateService, stateService,
twoFactorService, twoFactorService,
billingAccountProfileStateService,
); );
this.cache = new BehaviorSubject(data); this.cache = new BehaviorSubject(data);

View File

@ -9,6 +9,7 @@ import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/a
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response"; import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -42,6 +43,7 @@ describe("SsoLoginStrategy", () => {
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>; let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let authRequestService: MockProxy<AuthRequestServiceAbstraction>; let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
let i18nService: MockProxy<I18nService>; let i18nService: MockProxy<I18nService>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let ssoLoginStrategy: SsoLoginStrategy; let ssoLoginStrategy: SsoLoginStrategy;
let credentials: SsoLoginCredentials; let credentials: SsoLoginCredentials;
@ -68,6 +70,7 @@ describe("SsoLoginStrategy", () => {
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>(); deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
authRequestService = mock<AuthRequestServiceAbstraction>(); authRequestService = mock<AuthRequestServiceAbstraction>();
i18nService = mock<I18nService>(); i18nService = mock<I18nService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
tokenService.getTwoFactorToken.mockResolvedValue(null); tokenService.getTwoFactorToken.mockResolvedValue(null);
appIdService.getAppId.mockResolvedValue(deviceId); appIdService.getAppId.mockResolvedValue(deviceId);
@ -88,6 +91,7 @@ describe("SsoLoginStrategy", () => {
deviceTrustCryptoService, deviceTrustCryptoService,
authRequestService, authRequestService,
i18nService, i18nService,
billingAccountProfileStateService,
); );
credentials = new SsoLoginCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); credentials = new SsoLoginCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId);
}); });

View File

@ -10,6 +10,7 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for
import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request"; import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { HttpStatusCode } from "@bitwarden/common/enums"; import { HttpStatusCode } from "@bitwarden/common/enums";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
@ -87,6 +88,7 @@ export class SsoLoginStrategy extends LoginStrategy {
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction,
private i18nService: I18nService, private i18nService: I18nService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
cryptoService, cryptoService,
@ -98,6 +100,7 @@ export class SsoLoginStrategy extends LoginStrategy {
logService, logService,
stateService, stateService,
twoFactorService, twoFactorService,
billingAccountProfileStateService,
); );
this.cache = new BehaviorSubject(data); this.cache = new BehaviorSubject(data);

View File

@ -4,6 +4,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -36,6 +37,7 @@ describe("UserApiLoginStrategy", () => {
let twoFactorService: MockProxy<TwoFactorService>; let twoFactorService: MockProxy<TwoFactorService>;
let keyConnectorService: MockProxy<KeyConnectorService>; let keyConnectorService: MockProxy<KeyConnectorService>;
let environmentService: MockProxy<EnvironmentService>; let environmentService: MockProxy<EnvironmentService>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let apiLogInStrategy: UserApiLoginStrategy; let apiLogInStrategy: UserApiLoginStrategy;
let credentials: UserApiLoginCredentials; let credentials: UserApiLoginCredentials;
@ -57,6 +59,7 @@ describe("UserApiLoginStrategy", () => {
twoFactorService = mock<TwoFactorService>(); twoFactorService = mock<TwoFactorService>();
keyConnectorService = mock<KeyConnectorService>(); keyConnectorService = mock<KeyConnectorService>();
environmentService = mock<EnvironmentService>(); environmentService = mock<EnvironmentService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
appIdService.getAppId.mockResolvedValue(deviceId); appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.getTwoFactorToken.mockResolvedValue(null); tokenService.getTwoFactorToken.mockResolvedValue(null);
@ -75,6 +78,7 @@ describe("UserApiLoginStrategy", () => {
twoFactorService, twoFactorService,
environmentService, environmentService,
keyConnectorService, keyConnectorService,
billingAccountProfileStateService,
); );
credentials = new UserApiLoginCredentials(apiClientId, apiClientSecret); credentials = new UserApiLoginCredentials(apiClientId, apiClientSecret);

View File

@ -7,6 +7,7 @@ import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request"; import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -48,6 +49,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
twoFactorService: TwoFactorService, twoFactorService: TwoFactorService,
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private keyConnectorService: KeyConnectorService, private keyConnectorService: KeyConnectorService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
cryptoService, cryptoService,
@ -59,6 +61,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
logService, logService,
stateService, stateService,
twoFactorService, twoFactorService,
billingAccountProfileStateService,
); );
this.cache = new BehaviorSubject(data); this.cache = new BehaviorSubject(data);
} }

View File

@ -7,6 +7,7 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response"; import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response";
import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request"; import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -34,6 +35,7 @@ describe("WebAuthnLoginStrategy", () => {
let logService!: MockProxy<LogService>; let logService!: MockProxy<LogService>;
let stateService!: MockProxy<StateService>; let stateService!: MockProxy<StateService>;
let twoFactorService!: MockProxy<TwoFactorService>; let twoFactorService!: MockProxy<TwoFactorService>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let webAuthnLoginStrategy!: WebAuthnLoginStrategy; let webAuthnLoginStrategy!: WebAuthnLoginStrategy;
@ -68,6 +70,7 @@ describe("WebAuthnLoginStrategy", () => {
logService = mock<LogService>(); logService = mock<LogService>();
stateService = mock<StateService>(); stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>(); twoFactorService = mock<TwoFactorService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
tokenService.getTwoFactorToken.mockResolvedValue(null); tokenService.getTwoFactorToken.mockResolvedValue(null);
appIdService.getAppId.mockResolvedValue(deviceId); appIdService.getAppId.mockResolvedValue(deviceId);
@ -84,6 +87,7 @@ describe("WebAuthnLoginStrategy", () => {
logService, logService,
stateService, stateService,
twoFactorService, twoFactorService,
billingAccountProfileStateService,
); );
// Create credentials // Create credentials

View File

@ -7,6 +7,7 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request"; import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -48,6 +49,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
logService: LogService, logService: LogService,
stateService: StateService, stateService: StateService,
twoFactorService: TwoFactorService, twoFactorService: TwoFactorService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
super( super(
cryptoService, cryptoService,
@ -59,6 +61,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
logService, logService,
stateService, stateService,
twoFactorService, twoFactorService,
billingAccountProfileStateService,
); );
this.cache = new BehaviorSubject(data); this.cache = new BehaviorSubject(data);

View File

@ -11,6 +11,7 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
@ -50,6 +51,7 @@ describe("LoginStrategyService", () => {
let policyService: MockProxy<PolicyService>; let policyService: MockProxy<PolicyService>;
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>; let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let authRequestService: MockProxy<AuthRequestServiceAbstraction>; let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let stateProvider: FakeGlobalStateProvider; let stateProvider: FakeGlobalStateProvider;
let loginStrategyCacheExpirationState: FakeGlobalState<Date | null>; let loginStrategyCacheExpirationState: FakeGlobalState<Date | null>;
@ -72,6 +74,7 @@ describe("LoginStrategyService", () => {
policyService = mock<PolicyService>(); policyService = mock<PolicyService>();
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>(); deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
authRequestService = mock<AuthRequestServiceAbstraction>(); authRequestService = mock<AuthRequestServiceAbstraction>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
stateProvider = new FakeGlobalStateProvider(); stateProvider = new FakeGlobalStateProvider();
sut = new LoginStrategyService( sut = new LoginStrategyService(
@ -93,6 +96,7 @@ describe("LoginStrategyService", () => {
deviceTrustCryptoService, deviceTrustCryptoService,
authRequestService, authRequestService,
stateProvider, stateProvider,
billingAccountProfileStateService,
); );
loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY); loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY);

View File

@ -20,6 +20,7 @@ import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request"; import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request"; import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
@ -101,6 +102,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
protected authRequestService: AuthRequestServiceAbstraction, protected authRequestService: AuthRequestServiceAbstraction,
protected stateProvider: GlobalStateProvider, protected stateProvider: GlobalStateProvider,
protected billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY); this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY);
this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY); this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY);
@ -355,6 +357,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.passwordStrengthService, this.passwordStrengthService,
this.policyService, this.policyService,
this, this,
this.billingAccountProfileStateService,
); );
case AuthenticationType.Sso: case AuthenticationType.Sso:
return new SsoLoginStrategy( return new SsoLoginStrategy(
@ -372,6 +375,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.deviceTrustCryptoService, this.deviceTrustCryptoService,
this.authRequestService, this.authRequestService,
this.i18nService, this.i18nService,
this.billingAccountProfileStateService,
); );
case AuthenticationType.UserApiKey: case AuthenticationType.UserApiKey:
return new UserApiLoginStrategy( return new UserApiLoginStrategy(
@ -387,6 +391,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.twoFactorService, this.twoFactorService,
this.environmentService, this.environmentService,
this.keyConnectorService, this.keyConnectorService,
this.billingAccountProfileStateService,
); );
case AuthenticationType.AuthRequest: case AuthenticationType.AuthRequest:
return new AuthRequestLoginStrategy( return new AuthRequestLoginStrategy(
@ -401,6 +406,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.stateService, this.stateService,
this.twoFactorService, this.twoFactorService,
this.deviceTrustCryptoService, this.deviceTrustCryptoService,
this.billingAccountProfileStateService,
); );
case AuthenticationType.WebAuthn: case AuthenticationType.WebAuthn:
return new WebAuthnLoginStrategy( return new WebAuthnLoginStrategy(
@ -414,6 +420,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService, this.logService,
this.stateService, this.stateService,
this.twoFactorService, this.twoFactorService,
this.billingAccountProfileStateService,
); );
} }
}), }),

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

View File

@ -61,11 +61,6 @@ export abstract class StateService<T extends Account = Account> {
setAutoConfirmFingerprints: (value: boolean, options?: StorageOptions) => Promise<void>; setAutoConfirmFingerprints: (value: boolean, options?: StorageOptions) => Promise<void>;
getBiometricFingerprintValidated: (options?: StorageOptions) => Promise<boolean>; getBiometricFingerprintValidated: (options?: StorageOptions) => Promise<boolean>;
setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise<void>; setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise<void>;
getCanAccessPremium: (options?: StorageOptions) => Promise<boolean>;
getHasPremiumPersonally: (options?: StorageOptions) => Promise<boolean>;
setHasPremiumPersonally: (value: boolean, options?: StorageOptions) => Promise<void>;
setHasPremiumFromOrganization: (value: boolean, options?: StorageOptions) => Promise<void>;
getHasPremiumFromOrganization: (options?: StorageOptions) => Promise<boolean>;
getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise<boolean>; getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise<boolean>;
setConvertAccountToKeyConnector: (value: boolean, options?: StorageOptions) => Promise<void>; setConvertAccountToKeyConnector: (value: boolean, options?: StorageOptions) => Promise<void>;
/** /**

View File

@ -172,8 +172,6 @@ export class AccountProfile {
emailVerified?: boolean; emailVerified?: boolean;
everBeenUnlocked?: boolean; everBeenUnlocked?: boolean;
forceSetPasswordReason?: ForceSetPasswordReason; forceSetPasswordReason?: ForceSetPasswordReason;
hasPremiumPersonally?: boolean;
hasPremiumFromOrganization?: boolean;
lastSync?: string; lastSync?: string;
userId?: string; userId?: string;
usesKeyConnector?: boolean; usesKeyConnector?: boolean;

View File

@ -338,72 +338,6 @@ export class StateService<
); );
} }
async getCanAccessPremium(options?: StorageOptions): Promise<boolean> {
if (!(await this.getIsAuthenticated(options))) {
return false;
}
return (
(await this.getHasPremiumPersonally(options)) ||
(await this.getHasPremiumFromOrganization(options))
);
}
async getHasPremiumPersonally(options?: StorageOptions): Promise<boolean> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
return account?.profile?.hasPremiumPersonally;
}
async setHasPremiumPersonally(value: boolean, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.profile.hasPremiumPersonally = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getHasPremiumFromOrganization(options?: StorageOptions): Promise<boolean> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
if (account.profile?.hasPremiumFromOrganization) {
return true;
}
// TODO: older server versions won't send the hasPremiumFromOrganization flag, so we're keeping the old logic
// for backwards compatibility. It can be removed after everyone has upgraded.
const organizations = await this.getOrganizations(options);
if (organizations == null) {
return false;
}
for (const id of Object.keys(organizations)) {
const o = organizations[id];
if (o.enabled && o.usersGetPremium && !o.isProviderUser) {
return true;
}
}
return false;
}
async setHasPremiumFromOrganization(value: boolean, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.profile.hasPremiumFromOrganization = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getConvertAccountToKeyConnector(options?: StorageOptions): Promise<boolean> { async getConvertAccountToKeyConnector(options?: StorageOptions): Promise<boolean> {
return ( return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))

View File

@ -23,6 +23,9 @@ export const ORGANIZATIONS_DISK = new StateDefinition("organizations", "disk");
export const POLICIES_DISK = new StateDefinition("policies", "disk"); export const POLICIES_DISK = new StateDefinition("policies", "disk");
export const PROVIDERS_DISK = new StateDefinition("providers", "disk"); export const PROVIDERS_DISK = new StateDefinition("providers", "disk");
// Billing
export const BILLING_DISK = new StateDefinition("billing", "disk");
// Auth // Auth
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory"); export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
@ -43,15 +46,11 @@ export const USER_NOTIFICATION_SETTINGS_DISK = new StateDefinition(
"disk", "disk",
); );
// Billing
export const DOMAIN_SETTINGS_DISK = new StateDefinition("domainSettings", "disk"); export const DOMAIN_SETTINGS_DISK = new StateDefinition("domainSettings", "disk");
export const AUTOFILL_SETTINGS_DISK = new StateDefinition("autofillSettings", "disk"); export const AUTOFILL_SETTINGS_DISK = new StateDefinition("autofillSettings", "disk");
export const AUTOFILL_SETTINGS_DISK_LOCAL = new StateDefinition("autofillSettingsLocal", "disk", { export const AUTOFILL_SETTINGS_DISK_LOCAL = new StateDefinition("autofillSettingsLocal", "disk", {
web: "disk-local", web: "disk-local",
}); });
export const BILLING_DISK = new StateDefinition("billing", "disk");
// Components // Components

View File

@ -33,6 +33,7 @@ import { MoveThemeToStateProviderMigrator } from "./migrations/35-move-theme-to-
import { VaultSettingsKeyMigrator } from "./migrations/36-move-show-card-and-identity-to-state-provider"; import { VaultSettingsKeyMigrator } from "./migrations/36-move-show-card-and-identity-to-state-provider";
import { AvatarColorMigrator } from "./migrations/37-move-avatar-color-to-state-providers"; import { AvatarColorMigrator } from "./migrations/37-move-avatar-color-to-state-providers";
import { TokenServiceStateProviderMigrator } from "./migrations/38-migrate-token-svc-to-state-provider"; import { TokenServiceStateProviderMigrator } from "./migrations/38-migrate-token-svc-to-state-provider";
import { MoveBillingAccountProfileMigrator } from "./migrations/39-move-billing-account-profile-to-state-providers";
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked"; import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
@ -42,7 +43,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
import { MinVersionMigrator } from "./migrations/min-version"; import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 3; export const MIN_VERSION = 3;
export const CURRENT_VERSION = 38; export const CURRENT_VERSION = 39;
export type MinVersion = typeof MIN_VERSION; export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() { export function createMigrationBuilder() {
@ -82,7 +83,8 @@ export function createMigrationBuilder() {
.with(MoveThemeToStateProviderMigrator, 34, 35) .with(MoveThemeToStateProviderMigrator, 34, 35)
.with(VaultSettingsKeyMigrator, 35, 36) .with(VaultSettingsKeyMigrator, 35, 36)
.with(AvatarColorMigrator, 36, 37) .with(AvatarColorMigrator, 36, 37)
.with(TokenServiceStateProviderMigrator, 37, CURRENT_VERSION); .with(TokenServiceStateProviderMigrator, 37, 38)
.with(MoveBillingAccountProfileMigrator, 38, CURRENT_VERSION);
} }
export async function currentVersion( export async function currentVersion(

View File

@ -0,0 +1,126 @@
import { MockProxy, any } from "jest-mock-extended";
import { MigrationHelper } from "../migration-helper";
import { mockMigrationHelper } from "../migration-helper.spec";
import {
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
MoveBillingAccountProfileMigrator,
} from "./39-move-billing-account-profile-to-state-providers";
const exampleJSON = () => ({
global: {
otherStuff: "otherStuff1",
},
authenticatedAccounts: ["user-1", "user-2", "user-3"],
"user-1": {
profile: {
hasPremiumPersonally: true,
hasPremiumFromOrganization: false,
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
},
"user-2": {
otherStuff: "otherStuff4",
},
});
const rollbackJSON = () => ({
"user_user-1_billing_accountProfile": {
hasPremiumPersonally: true,
hasPremiumFromOrganization: false,
},
global: {
otherStuff: "otherStuff1",
},
authenticatedAccounts: ["user-1", "user-2", "user-3"],
"user-1": {
profile: {
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
},
"user-2": {
otherStuff: "otherStuff4",
},
});
describe("MoveBillingAccountProfileToStateProviders migrator", () => {
let helper: MockProxy<MigrationHelper>;
let sut: MoveBillingAccountProfileMigrator;
describe("migrate", () => {
beforeEach(() => {
helper = mockMigrationHelper(exampleJSON(), 39);
sut = new MoveBillingAccountProfileMigrator(38, 39);
});
it("removes from all accounts", async () => {
await sut.migrate(helper);
expect(helper.set).toHaveBeenCalledTimes(1);
expect(helper.set).toHaveBeenCalledWith("user-1", {
profile: {
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
});
});
it("sets hasPremiumPersonally value for account that have it", async () => {
await sut.migrate(helper);
expect(helper.setToUser).toHaveBeenCalledWith(
"user-1",
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
{ hasPremiumFromOrganization: false, hasPremiumPersonally: true },
);
});
it("should not call extra setToUser", async () => {
await sut.migrate(helper);
expect(helper.setToUser).toHaveBeenCalledTimes(1);
});
});
describe("rollback", () => {
beforeEach(() => {
helper = mockMigrationHelper(rollbackJSON(), 39);
sut = new MoveBillingAccountProfileMigrator(38, 39);
});
it("nulls out new values", async () => {
await sut.rollback(helper);
expect(helper.setToUser).toHaveBeenCalledWith(
"user-1",
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
null,
);
});
it("adds explicit value back to accounts", async () => {
await sut.rollback(helper);
expect(helper.set).toHaveBeenCalledTimes(1);
expect(helper.set).toHaveBeenCalledWith("user-1", {
profile: {
hasPremiumPersonally: true,
hasPremiumFromOrganization: false,
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
});
});
it.each(["user-2", "user-3"])(
"does not restore values when accounts are not present",
async (userId) => {
await sut.rollback(helper);
expect(helper.set).not.toHaveBeenCalledWith(userId, any());
},
);
});
});

View File

@ -0,0 +1,67 @@
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
import { Migrator } from "../migrator";
type ExpectedAccountType = {
profile?: {
hasPremiumPersonally?: boolean;
hasPremiumFromOrganization?: boolean;
};
};
type ExpectedBillingAccountProfileType = {
hasPremiumPersonally: boolean;
hasPremiumFromOrganization: boolean;
};
export const BILLING_ACCOUNT_PROFILE_KEY_DEFINITION: KeyDefinitionLike = {
key: "accountProfile",
stateDefinition: {
name: "billing",
},
};
export class MoveBillingAccountProfileMigrator extends Migrator<38, 39> {
async migrate(helper: MigrationHelper): Promise<void> {
const accounts = await helper.getAccounts<ExpectedAccountType>();
const migrateAccount = async (userId: string, account: ExpectedAccountType): Promise<void> => {
const hasPremiumPersonally = account?.profile?.hasPremiumPersonally;
const hasPremiumFromOrganization = account?.profile?.hasPremiumFromOrganization;
if (hasPremiumPersonally != null || hasPremiumFromOrganization != null) {
await helper.setToUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, {
hasPremiumPersonally: hasPremiumPersonally,
hasPremiumFromOrganization: hasPremiumFromOrganization,
});
delete account?.profile?.hasPremiumPersonally;
delete account?.profile?.hasPremiumFromOrganization;
await helper.set(userId, account);
}
};
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
}
async rollback(helper: MigrationHelper): Promise<void> {
const accounts = await helper.getAccounts<ExpectedAccountType>();
const rollbackAccount = async (userId: string, account: ExpectedAccountType): Promise<void> => {
const value = await helper.getFromUser<ExpectedBillingAccountProfileType>(
userId,
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
);
if (account && value) {
account.profile = Object.assign(account.profile ?? {}, {
hasPremiumPersonally: value?.hasPremiumPersonally,
hasPremiumFromOrganization: value?.hasPremiumFromOrganization,
});
await helper.set(userId, account);
}
await helper.setToUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, null);
};
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
}
}

View File

@ -11,6 +11,7 @@ import { AvatarService } from "../../../auth/abstractions/avatar.service";
import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service"; import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service";
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
import { BillingAccountProfileStateService } from "../../../billing/abstractions/account/billing-account-profile-state.service";
import { DomainsResponse } from "../../../models/response/domains.response"; import { DomainsResponse } from "../../../models/response/domains.response";
import { import {
SyncCipherNotification, SyncCipherNotification,
@ -62,6 +63,7 @@ export class SyncService implements SyncServiceAbstraction {
private sendApiService: SendApiService, private sendApiService: SendApiService,
private avatarService: AvatarService, private avatarService: AvatarService,
private logoutCallback: (expired: boolean) => Promise<void>, private logoutCallback: (expired: boolean) => Promise<void>,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) {} ) {}
async getLastSync(): Promise<Date> { async getLastSync(): Promise<Date> {
@ -314,8 +316,11 @@ export class SyncService implements SyncServiceAbstraction {
await this.avatarService.setAvatarColor(response.avatarColor); await this.avatarService.setAvatarColor(response.avatarColor);
await this.stateService.setSecurityStamp(response.securityStamp); await this.stateService.setSecurityStamp(response.securityStamp);
await this.stateService.setEmailVerified(response.emailVerified); await this.stateService.setEmailVerified(response.emailVerified);
await this.stateService.setHasPremiumPersonally(response.premiumPersonally);
await this.stateService.setHasPremiumFromOrganization(response.premiumFromOrganization); await this.billingAccountProfileStateService.setHasPremium(
response.premiumPersonally,
response.premiumFromOrganization,
);
await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector); await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector);
await this.setForceSetPasswordReasonIfNeeded(response); await this.setForceSetPasswordReasonIfNeeded(response);