1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-14366] Deprecated active user state from billing state service (#12273)

* Updated billing state provider to not rely on ActiveUserStateProvider

* Updated usages

* Resolved browser build

* Resolved web build

* Resolved CLI build

* resolved desktop build

* Update apps/cli/src/tools/send/commands/create.command.ts

Co-authored-by:  Audrey  <ajensen@bitwarden.com>

* Move subscription visibility logic from component to service

* Resolved unit test failures. Using existing userIds where present

* Simplified activeUserId access

* Resolved typescript strict errors

* Resolved broken unit test

* Resolved ts strict error

---------

Co-authored-by:  Audrey  <ajensen@bitwarden.com>
This commit is contained in:
Conner Turnbull
2025-01-07 10:25:26 -05:00
committed by GitHub
parent 003f5fdae9
commit 91d6963074
56 changed files with 595 additions and 227 deletions

View File

@@ -1,12 +1,14 @@
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs"; import { of } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
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 { 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 { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -19,6 +21,7 @@ describe("context-menu", () => {
let i18nService: MockProxy<I18nService>; let i18nService: MockProxy<I18nService>;
let logService: MockProxy<LogService>; let logService: MockProxy<LogService>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let accountService: MockProxy<AccountService>;
let removeAllSpy: jest.SpyInstance<void, [callback?: () => void]>; let removeAllSpy: jest.SpyInstance<void, [callback?: () => void]>;
let createSpy: jest.SpyInstance< let createSpy: jest.SpyInstance<
@@ -34,6 +37,7 @@ describe("context-menu", () => {
i18nService = mock(); i18nService = mock();
logService = mock(); logService = mock();
billingAccountProfileStateService = mock(); billingAccountProfileStateService = mock();
accountService = mock();
removeAllSpy = jest removeAllSpy = jest
.spyOn(chrome.contextMenus, "removeAll") .spyOn(chrome.contextMenus, "removeAll")
@@ -53,8 +57,15 @@ describe("context-menu", () => {
i18nService, i18nService,
logService, logService,
billingAccountProfileStateService, billingAccountProfileStateService,
accountService,
); );
autofillSettingsService.enableContextMenu$ = of(true); autofillSettingsService.enableContextMenu$ = of(true);
accountService.activeAccount$ = of({
id: "userId" as UserId,
email: "",
emailVerified: false,
name: undefined,
});
}); });
afterEach(() => jest.resetAllMocks()); afterEach(() => jest.resetAllMocks());
@@ -69,7 +80,7 @@ describe("context-menu", () => {
}); });
it("has menu enabled, but does not have premium", async () => { it("has menu enabled, but does not have premium", async () => {
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false); billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false));
const createdMenu = await sut.init(); const createdMenu = await sut.init();
expect(createdMenu).toBeTruthy(); expect(createdMenu).toBeTruthy();
@@ -77,7 +88,7 @@ describe("context-menu", () => {
}); });
it("has menu enabled and has premium", async () => { it("has menu enabled and has premium", async () => {
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
const createdMenu = await sut.init(); const createdMenu = await sut.init();
expect(createdMenu).toBeTruthy(); expect(createdMenu).toBeTruthy();
@@ -131,16 +142,15 @@ describe("context-menu", () => {
}); });
it("create entry for each cipher piece", async () => { it("create entry for each cipher piece", async () => {
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
await sut.loadOptions("TEST_TITLE", "1", createCipher()); await sut.loadOptions("TEST_TITLE", "1", createCipher());
// One for autofill, copy username, copy password, and copy totp code
expect(createSpy).toHaveBeenCalledTimes(4); expect(createSpy).toHaveBeenCalledTimes(4);
}); });
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 () => {
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
await sut.loadOptions("TEST_TITLE", "NOOP"); await sut.loadOptions("TEST_TITLE", "NOOP");

View File

@@ -2,6 +2,7 @@
// @ts-strict-ignore // @ts-strict-ignore
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { import {
AUTOFILL_CARD_ID, AUTOFILL_CARD_ID,
AUTOFILL_ID, AUTOFILL_ID,
@@ -149,6 +150,7 @@ export class MainContextMenuHandler {
private i18nService: I18nService, private i18nService: I18nService,
private logService: LogService, private logService: LogService,
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
private accountService: AccountService,
) {} ) {}
/** /**
@@ -168,11 +170,13 @@ export class MainContextMenuHandler {
this.initRunning = true; this.initRunning = true;
try { try {
const account = await firstValueFrom(this.accountService.activeAccount$);
const hasPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
);
for (const options of this.initContextMenuItems) { for (const options of this.initContextMenuItems) {
if ( if (options.checkPremiumAccess && !hasPremium) {
options.checkPremiumAccess &&
!(await firstValueFrom(this.billingAccountProfileStateService.hasPremiumFromAnySource$))
) {
continue; continue;
} }
@@ -267,8 +271,9 @@ export class MainContextMenuHandler {
await createChildItem(COPY_USERNAME_ID); await createChildItem(COPY_USERNAME_ID);
} }
const account = await firstValueFrom(this.accountService.activeAccount$);
const canAccessPremium = await firstValueFrom( const canAccessPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$, this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
); );
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

@@ -1,4 +1,4 @@
import { mock, mockReset, MockProxy } from "jest-mock-extended"; import { mock, MockProxy, mockReset } from "jest-mock-extended";
import { BehaviorSubject, of, Subject } from "rxjs"; import { BehaviorSubject, of, Subject } from "rxjs";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
@@ -730,7 +730,9 @@ 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); jest
.spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$")
.mockImplementation(() => of(true));
try { try {
await autofillService.doAutoFill(autofillOptions); await autofillService.doAutoFill(autofillOptions);
@@ -912,7 +914,9 @@ 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";
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); jest
.spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$")
.mockImplementation(() => 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);
@@ -925,7 +929,9 @@ 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";
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false); jest
.spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$")
.mockImplementation(() => 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);
@@ -959,7 +965,9 @@ 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;
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false); jest
.spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$")
.mockImplementation(() => of(false));
const autofillResult = await autofillService.doAutoFill(autofillOptions); const autofillResult = await autofillService.doAutoFill(autofillOptions);
@@ -969,7 +977,9 @@ 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;
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); jest
.spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$")
.mockImplementation(() => of(true));
jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(false); jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(false);
jest.spyOn(totpService, "getCode"); jest.spyOn(totpService, "getCode");

View File

@@ -416,8 +416,9 @@ export default class AutofillService implements AutofillServiceInterface {
let totp: string | null = null; let totp: string | null = null;
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
const canAccessPremium = await firstValueFrom( const canAccessPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$, this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeAccount.id),
); );
const defaultUriMatch = await this.getDefaultUriMatchStrategy(); const defaultUriMatch = await this.getDefaultUriMatchStrategy();

View File

@@ -792,6 +792,8 @@ export default class MainBackground {
this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService(
this.stateProvider, this.stateProvider,
this.platformUtilsService,
this.apiService,
); );
this.ssoLoginService = new SsoLoginService(this.stateProvider); this.ssoLoginService = new SsoLoginService(this.stateProvider);
@@ -1229,6 +1231,7 @@ export default class MainBackground {
this.i18nService, this.i18nService,
this.logService, this.logService,
this.billingAccountProfileStateService, this.billingAccountProfileStateService,
this.accountService,
); );
this.cipherContextMenuHandler = new CipherContextMenuHandler( this.cipherContextMenuHandler = new CipherContextMenuHandler(

View File

@@ -202,8 +202,11 @@ export default class RuntimeBackground {
return await this.configService.getFeatureFlag(FeatureFlag.InlineMenuFieldQualification); return await this.configService.getFeatureFlag(FeatureFlag.InlineMenuFieldQualification);
} }
case "getUserPremiumStatus": { case "getUserPremiumStatus": {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const result = await firstValueFrom( const result = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$, this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
); );
return result; return result;
} }

View File

@@ -7,6 +7,7 @@ import { RouterModule } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@@ -56,6 +57,7 @@ export class PremiumV2Component extends BasePremiumComponent {
dialogService: DialogService, dialogService: DialogService,
environmentService: EnvironmentService, environmentService: EnvironmentService,
billingAccountProfileStateService: BillingAccountProfileStateService, billingAccountProfileStateService: BillingAccountProfileStateService,
accountService: AccountService,
) { ) {
super( super(
i18nService, i18nService,
@@ -66,6 +68,7 @@ export class PremiumV2Component extends BasePremiumComponent {
dialogService, dialogService,
environmentService, environmentService,
billingAccountProfileStateService, billingAccountProfileStateService,
accountService,
); );
// 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

@@ -91,7 +91,17 @@ describe("SendV2Component", () => {
CurrentAccountComponent, CurrentAccountComponent,
], ],
providers: [ providers: [
{ provide: AccountService, useValue: mock<AccountService>() }, {
provide: AccountService,
useValue: {
activeAccount$: of({
id: "123",
email: "test@email.com",
emailVerified: true,
name: "Test User",
}),
},
},
{ provide: AuthService, useValue: mock<AuthService>() }, { provide: AuthService, useValue: mock<AuthService>() },
{ provide: AvatarService, useValue: mock<AvatarService>() }, { provide: AvatarService, useValue: mock<AvatarService>() },
{ {

View File

@@ -1,10 +1,11 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { RouterModule } from "@angular/router"; import { RouterModule } from "@angular/router";
import { Observable, firstValueFrom } from "rxjs"; import { Observable, firstValueFrom, of, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.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 { DialogService, ItemModule } from "@bitwarden/components"; import { DialogService, ItemModule } from "@bitwarden/components";
@@ -36,12 +37,19 @@ export class MoreFromBitwardenPageV2Component {
constructor( constructor(
private dialogService: DialogService, private dialogService: DialogService,
billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
private familiesPolicyService: FamiliesPolicyService, private familiesPolicyService: FamiliesPolicyService,
private accountService: AccountService,
) { ) {
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
switchMap((account) =>
account
? this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id)
: of(false),
),
);
this.familySponsorshipAvailable$ = this.organizationService.familySponsorshipAvailable$; this.familySponsorshipAvailable$ = this.organizationService.familySponsorshipAvailable$;
this.hasSingleEnterpriseOrg$ = this.familiesPolicyService.hasSingleEnterpriseOrg$(); this.hasSingleEnterpriseOrg$ = this.familiesPolicyService.hasSingleEnterpriseOrg$();
this.isFreeFamilyPolicyEnabled$ = this.familiesPolicyService.isFreeFamilyPolicyEnabled$(); this.isFreeFamilyPolicyEnabled$ = this.familiesPolicyService.isFreeFamilyPolicyEnabled$();

View File

@@ -1,7 +1,7 @@
import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { RouterTestingModule } from "@angular/router/testing"; import { RouterTestingModule } from "@angular/router/testing";
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject, of } from "rxjs";
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";
@@ -10,7 +10,6 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
import { ProductTierType } from "@bitwarden/common/billing/enums"; import { ProductTierType } from "@bitwarden/common/billing/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
@@ -55,7 +54,14 @@ describe("OpenAttachmentsComponent", () => {
const showFilePopoutMessage = jest.fn().mockReturnValue(false); const showFilePopoutMessage = jest.fn().mockReturnValue(false);
const mockUserId = Utils.newGuid() as UserId; const mockUserId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); const accountService = {
activeAccount$: of({
id: mockUserId,
email: "test@email.com",
emailVerified: true,
name: "Test User",
}),
};
beforeEach(async () => { beforeEach(async () => {
openCurrentPagePopout.mockClear(); openCurrentPagePopout.mockClear();
@@ -63,6 +69,7 @@ describe("OpenAttachmentsComponent", () => {
showToast.mockClear(); showToast.mockClear();
getOrganization.mockClear(); getOrganization.mockClear();
showFilePopoutMessage.mockClear(); showFilePopoutMessage.mockClear();
hasPremiumFromAnySource$.next(true);
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [OpenAttachmentsComponent, RouterTestingModule], imports: [OpenAttachmentsComponent, RouterTestingModule],
@@ -96,7 +103,7 @@ describe("OpenAttachmentsComponent", () => {
}).compileComponents(); }).compileComponents();
}); });
beforeEach(() => { beforeEach(async () => {
fixture = TestBed.createComponent(OpenAttachmentsComponent); fixture = TestBed.createComponent(OpenAttachmentsComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
component.cipherId = "5555-444-3333" as CipherId; component.cipherId = "5555-444-3333" as CipherId;
@@ -107,7 +114,7 @@ describe("OpenAttachmentsComponent", () => {
it("opens attachments in new popout", async () => { it("opens attachments in new popout", async () => {
showFilePopoutMessage.mockReturnValue(true); showFilePopoutMessage.mockReturnValue(true);
component.canAccessAttachments = true;
await component.ngOnInit(); await component.ngOnInit();
await component.openAttachments(); await component.openAttachments();
@@ -120,7 +127,7 @@ describe("OpenAttachmentsComponent", () => {
it("opens attachments in same window", async () => { it("opens attachments in same window", async () => {
showFilePopoutMessage.mockReturnValue(false); showFilePopoutMessage.mockReturnValue(false);
component.canAccessAttachments = true;
await component.ngOnInit(); await component.ngOnInit();
await component.openAttachments(); await component.openAttachments();

View File

@@ -4,7 +4,7 @@ import { CommonModule } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core"; import { Component, Input, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { firstValueFrom, map } from "rxjs"; import { firstValueFrom, map, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@@ -54,8 +54,13 @@ export class OpenAttachmentsComponent implements OnInit {
private filePopoutUtilsService: FilePopoutUtilsService, private filePopoutUtilsService: FilePopoutUtilsService,
private accountService: AccountService, private accountService: AccountService,
) { ) {
this.billingAccountProfileStateService.hasPremiumFromAnySource$ this.accountService.activeAccount$
.pipe(takeUntilDestroyed()) .pipe(
switchMap((account) =>
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
),
takeUntilDestroyed(),
)
.subscribe((canAccessPremium) => { .subscribe((canAccessPremium) => {
this.canAccessAttachments = canAccessPremium; this.canAccessAttachments = canAccessPremium;
}); });

View File

@@ -262,8 +262,9 @@ export class GetCommand extends DownloadCommand {
return Response.error("Couldn't generate TOTP code."); return Response.error("Couldn't generate TOTP code.");
} }
const account = await firstValueFrom(this.accountService.activeAccount$);
const canAccessPremium = await firstValueFrom( const canAccessPremium = await firstValueFrom(
this.accountProfileService.hasPremiumFromAnySource$, this.accountProfileService.hasPremiumFromAnySource$(account.id),
); );
if (!canAccessPremium) { if (!canAccessPremium) {
const originalCipher = await this.cipherService.get(cipher.id); const originalCipher = await this.cipherService.get(cipher.id);
@@ -347,8 +348,9 @@ export class GetCommand extends DownloadCommand {
return Response.multipleResults(attachments.map((a) => a.id)); return Response.multipleResults(attachments.map((a) => a.id));
} }
const account = await firstValueFrom(this.accountService.activeAccount$);
const canAccessPremium = await firstValueFrom( const canAccessPremium = await firstValueFrom(
this.accountProfileService.hasPremiumFromAnySource$, this.accountProfileService.hasPremiumFromAnySource$(account.id),
); );
if (!canAccessPremium) { if (!canAccessPremium) {
const originalCipher = await this.cipherService.get(cipher.id); const originalCipher = await this.cipherService.get(cipher.id);

View File

@@ -149,6 +149,7 @@ export class OssServeConfigurator {
this.serviceContainer.environmentService, this.serviceContainer.environmentService,
this.serviceContainer.sendApiService, this.serviceContainer.sendApiService,
this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.billingAccountProfileStateService,
this.serviceContainer.accountService,
); );
this.sendDeleteCommand = new SendDeleteCommand( this.sendDeleteCommand = new SendDeleteCommand(
this.serviceContainer.sendService, this.serviceContainer.sendService,
@@ -166,6 +167,7 @@ export class OssServeConfigurator {
this.sendGetCommand, this.sendGetCommand,
this.serviceContainer.sendApiService, this.serviceContainer.sendApiService,
this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.billingAccountProfileStateService,
this.serviceContainer.accountService,
); );
this.sendListCommand = new SendListCommand( this.sendListCommand = new SendListCommand(
this.serviceContainer.sendService, this.serviceContainer.sendService,

View File

@@ -597,6 +597,8 @@ export class ServiceContainer {
this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService(
this.stateProvider, this.stateProvider,
this.platformUtilsService,
this.apiService,
); );
this.taskSchedulerService = new DefaultTaskSchedulerService(this.logService); this.taskSchedulerService = new DefaultTaskSchedulerService(this.logService);

View File

@@ -3,8 +3,9 @@
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 { firstValueFrom, switchMap } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.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 { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
@@ -23,6 +24,7 @@ export class SendCreateCommand {
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private sendApiService: SendApiService, private sendApiService: SendApiService,
private accountProfileService: BillingAccountProfileStateService, private accountProfileService: BillingAccountProfileStateService,
private accountService: AccountService,
) {} ) {}
async run(requestJson: any, cmdOptions: Record<string, any>) { async run(requestJson: any, cmdOptions: Record<string, any>) {
@@ -78,6 +80,10 @@ export class SendCreateCommand {
req.key = null; req.key = null;
req.maxAccessCount = maxAccessCount; req.maxAccessCount = maxAccessCount;
const hasPremium$ = this.accountService.activeAccount$.pipe(
switchMap(({ id }) => this.accountProfileService.hasPremiumFromAnySource$(id)),
);
switch (req.type) { switch (req.type) {
case SendType.File: case SendType.File:
if (process.env.BW_SERVE === "true") { if (process.env.BW_SERVE === "true") {
@@ -86,7 +92,7 @@ export class SendCreateCommand {
); );
} }
if (!(await firstValueFrom(this.accountProfileService.hasPremiumFromAnySource$))) { if (!(await firstValueFrom(hasPremium$))) {
return Response.error("Premium status is required to use this feature."); return Response.error("Premium status is required to use this feature.");
} }

View File

@@ -2,6 +2,7 @@
// @ts-strict-ignore // @ts-strict-ignore
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; 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";
@@ -19,6 +20,7 @@ export class SendEditCommand {
private getCommand: SendGetCommand, private getCommand: SendGetCommand,
private sendApiService: SendApiService, private sendApiService: SendApiService,
private accountProfileService: BillingAccountProfileStateService, private accountProfileService: BillingAccountProfileStateService,
private accountService: AccountService,
) {} ) {}
async run(requestJson: string, cmdOptions: Record<string, any>): Promise<Response> { async run(requestJson: string, cmdOptions: Record<string, any>): Promise<Response> {
@@ -61,8 +63,9 @@ export class SendEditCommand {
return Response.badRequest("Cannot change a Send's type"); return Response.badRequest("Cannot change a Send's type");
} }
const account = await firstValueFrom(this.accountService.activeAccount$);
const canAccessPremium = await firstValueFrom( const canAccessPremium = await firstValueFrom(
this.accountProfileService.hasPremiumFromAnySource$, this.accountProfileService.hasPremiumFromAnySource$(account.id),
); );
if (send.type === SendType.File && !canAccessPremium) { 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

@@ -258,6 +258,7 @@ export class SendProgram extends BaseProgram {
getCmd, getCmd,
this.serviceContainer.sendApiService, this.serviceContainer.sendApiService,
this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.billingAccountProfileStateService,
this.serviceContainer.accountService,
); );
const response = await cmd.run(encodedJson, options); const response = await cmd.run(encodedJson, options);
this.processResponse(response); this.processResponse(response);
@@ -331,6 +332,7 @@ export class SendProgram extends BaseProgram {
this.serviceContainer.environmentService, this.serviceContainer.environmentService,
this.serviceContainer.sendApiService, this.serviceContainer.sendApiService,
this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.billingAccountProfileStateService,
this.serviceContainer.accountService,
); );
return await cmd.run(encodedJson, options); return await cmd.run(encodedJson, options);
} }

View File

@@ -136,10 +136,13 @@ export class CreateCommand {
return Response.notFound(); return Response.notFound();
} }
if ( const activeUserId = await firstValueFrom(this.activeUserId$);
cipher.organizationId == null &&
!(await firstValueFrom(this.accountProfileService.hasPremiumFromAnySource$)) const canAccessPremium = await firstValueFrom(
) { this.accountProfileService.hasPremiumFromAnySource$(activeUserId),
);
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.");
} }
@@ -152,7 +155,6 @@ export class CreateCommand {
} }
try { try {
const activeUserId = await firstValueFrom(this.activeUserId$);
const updatedCipher = await this.cipherService.saveAttachmentRawWithServer( const updatedCipher = await this.cipherService.saveAttachmentRawWithServer(
cipher, cipher,
fileName, fileName,

View File

@@ -89,8 +89,9 @@ export class DeleteCommand {
return Response.error("Attachment `" + id + "` was not found."); return Response.error("Attachment `" + id + "` was not found.");
} }
const account = await firstValueFrom(this.accountService.activeAccount$);
const canAccessPremium = await firstValueFrom( const canAccessPremium = await firstValueFrom(
this.accountProfileService.hasPremiumFromAnySource$, this.accountProfileService.hasPremiumFromAnySource$(account.id),
); );
if (cipher.organizationId == null && !canAccessPremium) { 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

@@ -2,13 +2,13 @@ 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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 { 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";
@Component({ @Component({
@@ -22,10 +22,10 @@ export class PremiumComponent extends BasePremiumComponent {
apiService: ApiService, apiService: ApiService,
configService: ConfigService, configService: ConfigService,
logService: LogService, logService: LogService,
stateService: StateService,
dialogService: DialogService, dialogService: DialogService,
environmentService: EnvironmentService, environmentService: EnvironmentService,
billingAccountProfileStateService: BillingAccountProfileStateService, billingAccountProfileStateService: BillingAccountProfileStateService,
accountService: AccountService,
) { ) {
super( super(
i18nService, i18nService,
@@ -36,6 +36,7 @@ export class PremiumComponent extends BasePremiumComponent {
dialogService, dialogService,
environmentService, environmentService,
billingAccountProfileStateService, billingAccountProfileStateService,
accountService,
); );
} }
} }

View File

@@ -10,7 +10,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 { Subject, takeUntil, switchMap } 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";
@@ -18,6 +18,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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -111,11 +112,17 @@ export class VaultComponent implements OnInit, OnDestroy {
private dialogService: DialogService, private dialogService: DialogService,
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
private configService: ConfigService, private configService: ConfigService,
private accountService: AccountService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
this.billingAccountProfileStateService.hasPremiumFromAnySource$ this.accountService.activeAccount$
.pipe(takeUntil(this.componentIsDestroyed$)) .pipe(
switchMap((account) =>
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
),
takeUntil(this.componentIsDestroyed$),
)
.subscribe((canAccessPremium: boolean) => { .subscribe((canAccessPremium: boolean) => {
this.userHasPremiumAccess = canAccessPremium; this.userHasPremiumAccess = canAccessPremium;
}); });

View File

@@ -10,6 +10,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service";
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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response"; import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response";
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
@@ -37,6 +38,7 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme
private route: ActivatedRoute, private route: ActivatedRoute,
private organizationService: OrganizationService, private organizationService: OrganizationService,
billingAccountProfileStateService: BillingAccountProfileStateService, billingAccountProfileStateService: BillingAccountProfileStateService,
accountService: AccountService,
) { ) {
super( super(
dialogService, dialogService,
@@ -45,6 +47,7 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme
messagingService, messagingService,
policyService, policyService,
billingAccountProfileStateService, billingAccountProfileStateService,
accountService,
); );
} }

View File

@@ -1,11 +1,12 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { lastValueFrom, Observable, firstValueFrom } from "rxjs"; import { lastValueFrom, Observable, firstValueFrom, switchMap } 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 { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.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";
@@ -69,8 +70,13 @@ export class EmergencyAccessComponent implements OnInit {
billingAccountProfileStateService: BillingAccountProfileStateService, billingAccountProfileStateService: BillingAccountProfileStateService,
protected organizationManagementPreferencesService: OrganizationManagementPreferencesService, protected organizationManagementPreferencesService: OrganizationManagementPreferencesService,
private toastService: ToastService, private toastService: ToastService,
private accountService: AccountService,
) { ) {
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
switchMap((account) =>
billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
),
);
} }
async ngOnInit() { async ngOnInit() {

View File

@@ -10,6 +10,7 @@ import {
Subject, Subject,
Subscription, Subscription,
takeUntil, takeUntil,
switchMap,
} from "rxjs"; } from "rxjs";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
@@ -18,6 +19,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
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 { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/auth/models/response/two-factor-authenticator.response"; import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/auth/models/response/two-factor-authenticator.response";
import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response"; import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response";
@@ -69,8 +71,13 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
protected messagingService: MessagingService, protected messagingService: MessagingService,
protected policyService: PolicyService, protected policyService: PolicyService,
billingAccountProfileStateService: BillingAccountProfileStateService, billingAccountProfileStateService: BillingAccountProfileStateService,
private accountService: AccountService,
) { ) {
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
switchMap((account) =>
billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
),
);
} }
async ngOnInit() { async ngOnInit() {

View File

@@ -4,10 +4,11 @@ import { Component, ViewChild } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl, FormGroup, Validators } from "@angular/forms"; import { FormControl, FormGroup, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { combineLatest, concatMap, from, Observable, of } from "rxjs"; import { combineLatest, concatMap, from, Observable, of, switchMap } from "rxjs";
import { debounceTime } from "rxjs/operators"; import { debounceTime } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
@@ -65,14 +66,22 @@ export class PremiumV2Component {
private toastService: ToastService, private toastService: ToastService,
private tokenService: TokenService, private tokenService: TokenService,
private taxService: TaxServiceAbstraction, private taxService: TaxServiceAbstraction,
private accountService: AccountService,
) { ) {
this.isSelfHost = this.platformUtilsService.isSelfHost(); this.isSelfHost = this.platformUtilsService.isSelfHost();
this.hasPremiumFromAnyOrganization$ = this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe(
this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$; switchMap((account) =>
this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id),
),
);
combineLatest([ combineLatest([
this.billingAccountProfileStateService.hasPremiumPersonally$, this.accountService.activeAccount$.pipe(
switchMap((account) =>
this.billingAccountProfileStateService.hasPremiumPersonally$(account.id),
),
),
this.environmentService.cloudWebVaultUrl$, this.environmentService.cloudWebVaultUrl$,
]) ])
.pipe( .pipe(

View File

@@ -4,10 +4,11 @@ import { Component, OnInit, ViewChild } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl, FormGroup, Validators } from "@angular/forms"; import { FormControl, FormGroup, Validators } from "@angular/forms";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { firstValueFrom, Observable } from "rxjs"; import { firstValueFrom, Observable, switchMap } from "rxjs";
import { debounceTime } from "rxjs/operators"; import { debounceTime } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
@@ -58,9 +59,14 @@ export class PremiumComponent implements OnInit {
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
private toastService: ToastService, private toastService: ToastService,
private taxService: TaxServiceAbstraction, private taxService: TaxServiceAbstraction,
private accountService: AccountService,
) { ) {
this.selfHosted = platformUtilsService.isSelfHost(); this.selfHosted = platformUtilsService.isSelfHost();
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
switchMap((account) =>
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
),
);
this.addonForm.controls.additionalStorage.valueChanges this.addonForm.controls.additionalStorage.valueChanges
.pipe(debounceTime(1000), takeUntilDestroyed()) .pipe(debounceTime(1000), takeUntilDestroyed())
@@ -75,7 +81,10 @@ export class PremiumComponent implements OnInit {
} }
async ngOnInit() { async ngOnInit() {
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$); this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
if (await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$)) { const account = await firstValueFrom(this.accountService.activeAccount$);
if (
await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$(account.id))
) {
// 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,8 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { Observable } from "rxjs"; import { Observable, switchMap } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; 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";
@@ -16,8 +17,11 @@ export class SubscriptionComponent implements OnInit {
constructor( constructor(
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
billingAccountProfileStateService: BillingAccountProfileStateService, billingAccountProfileStateService: BillingAccountProfileStateService,
accountService: AccountService,
) { ) {
this.hasPremium$ = billingAccountProfileStateService.hasPremiumPersonally$; this.hasPremium$ = accountService.activeAccount$.pipe(
switchMap((account) => billingAccountProfileStateService.hasPremiumPersonally$(account.id)),
);
} }
ngOnInit() { ngOnInit() {

View File

@@ -5,6 +5,7 @@ import { Router } from "@angular/router";
import { firstValueFrom, lastValueFrom } from "rxjs"; import { firstValueFrom, lastValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.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";
@@ -60,6 +61,7 @@ export class UserSubscriptionComponent implements OnInit {
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
private toastService: ToastService, private toastService: ToastService,
private configService: ConfigService, private configService: ConfigService,
private accountService: AccountService,
) { ) {
this.selfHosted = this.platformUtilsService.isSelfHost(); this.selfHosted = this.platformUtilsService.isSelfHost();
} }
@@ -75,7 +77,10 @@ export class UserSubscriptionComponent implements OnInit {
return; return;
} }
if (await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$)) { const userId = await firstValueFrom(this.accountService.activeAccount$);
if (
await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$(userId.id))
) {
this.loading = true; this.loading = true;
this.sub = await this.apiService.getUserSubscription(); this.sub = await this.apiService.getUserSubscription();
} else { } else {

View File

@@ -6,9 +6,10 @@ import {
CanActivateFn, CanActivateFn,
UrlTree, UrlTree,
} from "@angular/router"; } from "@angular/router";
import { Observable } from "rxjs"; import { Observable, of } from "rxjs";
import { tap } from "rxjs/operators"; import { switchMap, tap } from "rxjs/operators";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; 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";
@@ -24,8 +25,14 @@ export function hasPremiumGuard(): CanActivateFn {
const router = inject(Router); const router = inject(Router);
const messagingService = inject(MessagingService); const messagingService = inject(MessagingService);
const billingAccountProfileStateService = inject(BillingAccountProfileStateService); const billingAccountProfileStateService = inject(BillingAccountProfileStateService);
const accountService = inject(AccountService);
return billingAccountProfileStateService.hasPremiumFromAnySource$.pipe( return accountService.activeAccount$.pipe(
switchMap((account) =>
account
? billingAccountProfileStateService.hasPremiumFromAnySource$(account.id)
: of(false),
),
tap((userHasPremium: boolean) => { tap((userHasPremium: boolean) => {
if (!userHasPremium) { if (!userHasPremium) {
messagingService.send("premiumRequired"); messagingService.send("premiumRequired");

View File

@@ -3,12 +3,11 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { RouterModule } from "@angular/router"; import { RouterModule } from "@angular/router";
import { Observable, concatMap, combineLatest } from "rxjs"; import { Observable, switchMap } 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/platform/sync"; import { SyncService } from "@bitwarden/common/platform/sync";
import { IconModule } from "@bitwarden/components"; import { IconModule } from "@bitwarden/components";
@@ -38,35 +37,19 @@ export class UserLayoutComponent implements OnInit {
protected showSubscription$: Observable<boolean>; protected showSubscription$: Observable<boolean>;
constructor( constructor(
private platformUtilsService: PlatformUtilsService,
private apiService: ApiService,
private syncService: SyncService, private syncService: SyncService,
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
) {} private accountService: AccountService,
) {
this.showSubscription$ = this.accountService.activeAccount$.pipe(
switchMap((account) =>
this.billingAccountProfileStateService.canViewSubscription$(account.id),
),
);
}
async ngOnInit() { async ngOnInit() {
document.body.classList.remove("layout_frontend"); document.body.classList.remove("layout_frontend");
await this.syncService.fullSync(false); await this.syncService.fullSync(false);
// We want to hide the subscription menu for organizations that provide premium.
// Except if the user has premium personally or has a billing history.
this.showSubscription$ = combineLatest([
this.billingAccountProfileStateService.hasPremiumPersonally$,
this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$,
]).pipe(
concatMap(async ([hasPremiumPersonally, hasPremiumFromOrg]) => {
const isCloud = !this.platformUtilsService.isSelfHost();
let billing = null;
if (isCloud) {
// TODO: We should remove the need to call this!
billing = await this.apiService.getUserBillingHistory();
}
const cloudAndBillingHistory = isCloud && !billing?.hasNoHistory;
return hasPremiumPersonally || !hasPremiumFromOrg || cloudAndBillingHistory;
}),
);
} }
} }

View File

@@ -3,6 +3,7 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-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";
@@ -15,11 +16,15 @@ import { ReportEntry, ReportVariant } from "../shared";
export class ReportsHomeComponent implements OnInit { export class ReportsHomeComponent implements OnInit {
reports: ReportEntry[]; reports: ReportEntry[];
constructor(private billingAccountProfileStateService: BillingAccountProfileStateService) {} constructor(
private billingAccountProfileStateService: BillingAccountProfileStateService,
private accountService: AccountService,
) {}
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
const account = await firstValueFrom(this.accountService.activeAccount$);
const userHasPremium = await firstValueFrom( const userHasPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$, this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
); );
const reportRequiresPremium = userHasPremium const reportRequiresPremium = userHasPremium
? ReportVariant.Enabled ? ReportVariant.Enabled

View File

@@ -4,7 +4,7 @@ import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { firstValueFrom, Observable, Subject } from "rxjs"; import { firstValueFrom, Observable, Subject, switchMap } from "rxjs";
import { map } from "rxjs/operators"; import { map } from "rxjs/operators";
import { CollectionView } from "@bitwarden/admin-console/common"; import { CollectionView } from "@bitwarden/admin-console/common";
@@ -183,7 +183,11 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
* Flag to indicate if the user has access to attachments via a premium subscription. * Flag to indicate if the user has access to attachments via a premium subscription.
* @protected * @protected
*/ */
protected canAccessAttachments$ = this.billingAccountProfileStateService.hasPremiumFromAnySource$; protected canAccessAttachments$ = this.accountService.activeAccount$.pipe(
switchMap((account) =>
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
),
);
protected get loadingForm() { protected get loadingForm() {
return this.loadForm && !this.formReady; return this.loadForm && !this.formReady;

View File

@@ -9,6 +9,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/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 { PolicyType } from "@bitwarden/common/admin-console/enums"; 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -34,6 +35,7 @@ describe("AddEditComponentV2", () => {
let messagingService: MockProxy<MessagingService>; let messagingService: MockProxy<MessagingService>;
let folderService: MockProxy<FolderService>; let folderService: MockProxy<FolderService>;
let collectionService: MockProxy<CollectionService>; let collectionService: MockProxy<CollectionService>;
let accountService: MockProxy<AccountService>;
const mockParams = { const mockParams = {
cloneMode: false, cloneMode: false,
@@ -55,7 +57,9 @@ describe("AddEditComponentV2", () => {
); );
billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); billingAccountProfileStateService.hasPremiumFromAnySource$.mockImplementation((userId) =>
of(true),
);
activatedRoute = mock<ActivatedRoute>(); activatedRoute = mock<ActivatedRoute>();
activatedRoute.queryParams = of({}); activatedRoute.queryParams = of({});
@@ -68,6 +72,9 @@ describe("AddEditComponentV2", () => {
collectionService = mock<CollectionService>(); collectionService = mock<CollectionService>();
collectionService.decryptedCollections$ = of([]); collectionService.decryptedCollections$ = of([]);
accountService = mock<AccountService>();
accountService.activeAccount$ = of({ id: "test-id" } as any);
const mockDefaultCipherFormConfigService = { const mockDefaultCipherFormConfigService = {
buildConfig: jest.fn().mockResolvedValue({ buildConfig: jest.fn().mockResolvedValue({
allowPersonal: true, allowPersonal: true,
@@ -97,6 +104,7 @@ describe("AddEditComponentV2", () => {
provide: PasswordGenerationServiceAbstraction, provide: PasswordGenerationServiceAbstraction,
useValue: mock<PasswordGenerationServiceAbstraction>(), useValue: mock<PasswordGenerationServiceAbstraction>(),
}, },
{ provide: AccountService, useValue: accountService },
], ],
}).compileComponents(); }).compileComponents();

View File

@@ -4,8 +4,10 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component, Inject, OnInit } from "@angular/core"; import { Component, Inject, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { switchMap } from "rxjs";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { CipherId } from "@bitwarden/common/types/guid"; import { CipherId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
@@ -85,10 +87,16 @@ export class AddEditComponentV2 implements OnInit {
private i18nService: I18nService, private i18nService: I18nService,
private dialogService: DialogService, private dialogService: DialogService,
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
private accountService: AccountService,
) { ) {
this.billingAccountProfileStateService.hasPremiumFromAnySource$ this.accountService.activeAccount$
.pipe(takeUntilDestroyed()) .pipe(
.subscribe((canAccessPremium) => { switchMap((account) =>
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
),
takeUntilDestroyed(),
)
.subscribe((canAccessPremium: boolean) => {
this.canAccessAttachments = canAccessPremium; this.canAccessAttachments = canAccessPremium;
}); });
} }

View File

@@ -2,7 +2,7 @@
// @ts-strict-ignore // @ts-strict-ignore
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 { firstValueFrom, map } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common"; import { CollectionService } from "@bitwarden/admin-console/common";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component";
@@ -116,9 +116,14 @@ 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 firstValueFrom( const activeUserId = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$, this.accountService.activeAccount$.pipe(map((a) => a.id)),
); );
this.canAccessPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
);
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

@@ -1,6 +1,7 @@
import { TestBed } from "@angular/core/testing"; import { TestBed } from "@angular/core/testing";
import { BehaviorSubject, firstValueFrom } from "rxjs"; import { BehaviorSubject, firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
@@ -21,7 +22,8 @@ describe("VaultBannersService", () => {
let service: VaultBannersService; let service: VaultBannersService;
const isSelfHost = jest.fn().mockReturnValue(false); const isSelfHost = jest.fn().mockReturnValue(false);
const hasPremiumFromAnySource$ = new BehaviorSubject<boolean>(false); const hasPremiumFromAnySource$ = new BehaviorSubject<boolean>(false);
const fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId)); const userId = "user-id" as UserId;
const fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(userId));
const getEmailVerified = jest.fn().mockResolvedValue(true); const getEmailVerified = jest.fn().mockResolvedValue(true);
const hasMasterPassword = jest.fn().mockResolvedValue(true); const hasMasterPassword = jest.fn().mockResolvedValue(true);
const getKdfConfig = jest const getKdfConfig = jest
@@ -44,15 +46,15 @@ describe("VaultBannersService", () => {
}, },
{ {
provide: BillingAccountProfileStateService, provide: BillingAccountProfileStateService,
useValue: { hasPremiumFromAnySource$: hasPremiumFromAnySource$ }, useValue: { hasPremiumFromAnySource$: () => hasPremiumFromAnySource$ },
}, },
{ {
provide: StateProvider, provide: StateProvider,
useValue: fakeStateProvider, useValue: fakeStateProvider,
}, },
{ {
provide: PlatformUtilsService, provide: AccountService,
useValue: { isSelfHost }, useValue: mockAccountServiceWith(userId),
}, },
{ {
provide: TokenService, provide: TokenService,

View File

@@ -1,7 +1,17 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { Subject, Observable, combineLatest, firstValueFrom, map } from "rxjs"; import {
import { mergeMap, take } from "rxjs/operators"; Subject,
Observable,
combineLatest,
firstValueFrom,
map,
mergeMap,
take,
switchMap,
of,
} from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
@@ -74,15 +84,23 @@ export class VaultBannersService {
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private kdfConfigService: KdfConfigService, private kdfConfigService: KdfConfigService,
private syncService: SyncService, private syncService: SyncService,
private accountService: AccountService,
) { ) {
this.pollUntilSynced(); this.pollUntilSynced();
this.premiumBannerState = this.stateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY); this.premiumBannerState = this.stateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY);
this.sessionBannerState = this.stateProvider.getActive(BANNERS_DISMISSED_DISK_KEY); this.sessionBannerState = this.stateProvider.getActive(BANNERS_DISMISSED_DISK_KEY);
const premiumSources$ = combineLatest([ const premiumSources$ = this.accountService.activeAccount$.pipe(
this.billingAccountProfileStateService.hasPremiumFromAnySource$, take(1),
this.premiumBannerState.state$, switchMap((account) => {
]); return combineLatest([
account
? this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id)
: of(false),
this.premiumBannerState.state$,
]);
}),
);
this.shouldShowPremiumBanner$ = this.syncCompleted$.pipe( this.shouldShowPremiumBanner$ = this.syncCompleted$.pipe(
take(1), // Wait until the first sync is complete before considering the premium status take(1), // Wait until the first sync is complete before considering the premium status

View File

@@ -467,7 +467,7 @@ export class VaultComponent implements OnInit, OnDestroy {
switchMap(() => switchMap(() =>
combineLatest([ combineLatest([
filter$, filter$,
this.billingAccountProfileStateService.hasPremiumFromAnySource$, this.billingAccountProfileStateService.hasPremiumFromAnySource$(this.activeUserId),
allCollections$, allCollections$,
this.organizationService.organizations$, this.organizationService.organizations$,
ciphers$, ciphers$,

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 { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
/** /**
@@ -14,11 +15,19 @@ export class NotPremiumDirective implements OnInit {
private templateRef: TemplateRef<any>, private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef, private viewContainer: ViewContainerRef,
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
private accountService: AccountService,
) {} ) {}
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
const account = await firstValueFrom(this.accountService.activeAccount$);
if (!account) {
this.viewContainer.createEmbeddedView(this.templateRef);
return;
}
const premium = await firstValueFrom( const premium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$, this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
); );
if (premium) { if (premium) {

View File

@@ -1,6 +1,7 @@
import { Directive, OnDestroy, OnInit, TemplateRef, ViewContainerRef } from "@angular/core"; import { Directive, OnDestroy, OnInit, TemplateRef, ViewContainerRef } from "@angular/core";
import { Subject, takeUntil } from "rxjs"; import { of, Subject, switchMap, takeUntil } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
/** /**
@@ -16,16 +17,24 @@ export class PremiumDirective implements OnInit, OnDestroy {
private templateRef: TemplateRef<any>, private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef, private viewContainer: ViewContainerRef,
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
private accountService: AccountService,
) {} ) {}
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
this.billingAccountProfileStateService.hasPremiumFromAnySource$ this.accountService.activeAccount$
.pipe(takeUntil(this.directiveIsDestroyed$)) .pipe(
switchMap((account) =>
account
? this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id)
: of(false),
),
takeUntil(this.directiveIsDestroyed$),
)
.subscribe((premium: boolean) => { .subscribe((premium: boolean) => {
if (premium) { if (premium) {
this.viewContainer.clear();
} else {
this.viewContainer.createEmbeddedView(this.templateRef); this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
} }
}); });
} }

View File

@@ -1281,7 +1281,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({ safeProvider({
provide: BillingAccountProfileStateService, provide: BillingAccountProfileStateService,
useClass: DefaultBillingAccountProfileStateService, useClass: DefaultBillingAccountProfileStateService,
deps: [StateProvider], deps: [StateProvider, PlatformUtilsServiceAbstraction, ApiServiceAbstraction],
}), }),
safeProvider({ safeProvider({
provide: OrganizationManagementPreferencesService, provide: OrganizationManagementPreferencesService,

View File

@@ -3,7 +3,15 @@
import { DatePipe } from "@angular/common"; import { DatePipe } from "@angular/common";
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms"; import { FormBuilder, Validators } from "@angular/forms";
import { Subject, firstValueFrom, takeUntil, map, BehaviorSubject, concatMap } from "rxjs"; import {
Subject,
firstValueFrom,
takeUntil,
map,
BehaviorSubject,
concatMap,
switchMap,
} from "rxjs";
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";
@@ -197,8 +205,13 @@ export class AddEditComponent implements OnInit, OnDestroy {
const env = await firstValueFrom(this.environmentService.environment$); const env = await firstValueFrom(this.environmentService.environment$);
this.sendLinkBaseUrl = env.getSendUrl(); this.sendLinkBaseUrl = env.getSendUrl();
this.billingAccountProfileStateService.hasPremiumFromAnySource$ this.accountService.activeAccount$
.pipe(takeUntil(this.destroy$)) .pipe(
switchMap((account) =>
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
),
takeUntil(this.destroy$),
)
.subscribe((hasPremiumFromAnySource) => { .subscribe((hasPremiumFromAnySource) => {
this.canAccessPremium = hasPremiumFromAnySource; this.canAccessPremium = hasPremiumFromAnySource;
}); });

View File

@@ -209,7 +209,7 @@ export class AttachmentsComponent implements OnInit {
); );
const canAccessPremium = await firstValueFrom( const canAccessPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$, this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
); );
this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null; this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null;

View File

@@ -1,9 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { OnInit, Directive } from "@angular/core"; import { OnInit, Directive } from "@angular/core";
import { firstValueFrom, Observable } from "rxjs"; import { firstValueFrom, Observable, switchMap } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@@ -30,8 +31,13 @@ export class PremiumComponent implements OnInit {
protected dialogService: DialogService, protected dialogService: DialogService,
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
billingAccountProfileStateService: BillingAccountProfileStateService, billingAccountProfileStateService: BillingAccountProfileStateService,
accountService: AccountService,
) { ) {
this.isPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; this.isPremium$ = accountService.activeAccount$.pipe(
switchMap((account) =>
billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
),
);
} }
async ngOnInit() { async ngOnInit() {

View File

@@ -148,7 +148,7 @@ export class ViewComponent implements OnDestroy, OnInit {
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
); );
this.canAccessPremium = await firstValueFrom( this.canAccessPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$, this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
); );
this.showPremiumRequiredTotp = this.showPremiumRequiredTotp =
this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp; this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp;

View File

@@ -11,27 +11,32 @@ export type BillingAccountProfile = {
export abstract class BillingAccountProfileStateService { export abstract class BillingAccountProfileStateService {
/** /**
* Emits `true` when the active user's account has been granted premium from any of the * Emits `true` when the user's account has been granted premium from any of the
* organizations it is a member of. Otherwise, emits `false` * organizations it is a member of. Otherwise, emits `false`
*/ */
hasPremiumFromAnyOrganization$: Observable<boolean>; abstract hasPremiumFromAnyOrganization$(userId: UserId): Observable<boolean>;
/** /**
* Emits `true` when the active user's account has an active premium subscription at the * Emits `true` when the user's account has an active premium subscription at the
* individual user level * individual user level
*/ */
hasPremiumPersonally$: Observable<boolean>; abstract hasPremiumPersonally$(userId: UserId): Observable<boolean>;
/** /**
* Emits `true` when either `hasPremiumPersonally` or `hasPremiumFromAnyOrganization` is `true` * Emits `true` when either `hasPremiumPersonally` or `hasPremiumFromAnyOrganization` is `true`
*/ */
hasPremiumFromAnySource$: Observable<boolean>; abstract hasPremiumFromAnySource$(userId: UserId): Observable<boolean>;
/** /**
* Sets the active user's premium status fields upon every full sync, either from their personal * Emits `true` when the subscription menu item should be shown in navigation.
* This is hidden for organizations that provide premium, except if the user has premium personally
* or has a billing history.
*/
abstract canViewSubscription$(userId: UserId): Observable<boolean>;
/**
* Sets the 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. * subscription to premium, or an organization they're a part of that grants them premium.
* @param hasPremiumPersonally
* @param hasPremiumFromAnyOrganization
*/ */
abstract setHasPremium( abstract setHasPremium(
hasPremiumPersonally: boolean, hasPremiumPersonally: boolean,

View File

@@ -1,5 +1,9 @@
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingHistoryResponse } from "@bitwarden/common/billing/models/response/billing-history.response";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { import {
FakeAccountService, FakeAccountService,
mockAccountServiceWith, mockAccountServiceWith,
@@ -19,14 +23,26 @@ describe("BillingAccountProfileStateService", () => {
let sut: DefaultBillingAccountProfileStateService; let sut: DefaultBillingAccountProfileStateService;
let userBillingAccountProfileState: FakeSingleUserState<BillingAccountProfile>; let userBillingAccountProfileState: FakeSingleUserState<BillingAccountProfile>;
let accountService: FakeAccountService; let accountService: FakeAccountService;
let platformUtilsService: jest.Mocked<PlatformUtilsService>;
let apiService: jest.Mocked<ApiService>;
const userId = "fakeUserId" as UserId; const userId = "fakeUserId" as UserId;
beforeEach(() => { beforeEach(() => {
accountService = mockAccountServiceWith(userId); accountService = mockAccountServiceWith(userId);
stateProvider = new FakeStateProvider(accountService); stateProvider = new FakeStateProvider(accountService);
platformUtilsService = {
isSelfHost: jest.fn(),
} as any;
apiService = {
getUserBillingHistory: jest.fn(),
} as any;
sut = new DefaultBillingAccountProfileStateService(stateProvider); sut = new DefaultBillingAccountProfileStateService(
stateProvider,
platformUtilsService,
apiService,
);
userBillingAccountProfileState = stateProvider.singleUser.getFake( userBillingAccountProfileState = stateProvider.singleUser.getFake(
userId, userId,
@@ -45,7 +61,7 @@ describe("BillingAccountProfileStateService", () => {
hasPremiumFromAnyOrganization: true, hasPremiumFromAnyOrganization: true,
}); });
expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(true); expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$(userId))).toBe(true);
}); });
it("return false when they do not have premium from an organization", async () => { it("return false when they do not have premium from an organization", async () => {
@@ -54,13 +70,7 @@ describe("BillingAccountProfileStateService", () => {
hasPremiumFromAnyOrganization: false, hasPremiumFromAnyOrganization: false,
}); });
expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false); expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$(userId))).toBe(false);
});
it("returns false when there is no active user", async () => {
await accountService.switchAccount(null);
expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false);
}); });
}); });
@@ -71,7 +81,7 @@ describe("BillingAccountProfileStateService", () => {
hasPremiumFromAnyOrganization: false, hasPremiumFromAnyOrganization: false,
}); });
expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true); expect(await firstValueFrom(sut.hasPremiumPersonally$(userId))).toBe(true);
}); });
it("returns false when the user does not have premium personally", async () => { it("returns false when the user does not have premium personally", async () => {
@@ -80,13 +90,7 @@ describe("BillingAccountProfileStateService", () => {
hasPremiumFromAnyOrganization: false, hasPremiumFromAnyOrganization: false,
}); });
expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(false); expect(await firstValueFrom(sut.hasPremiumPersonally$(userId))).toBe(false);
});
it("returns false when there is no active user", async () => {
await accountService.switchAccount(null);
expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(false);
}); });
}); });
@@ -97,7 +101,7 @@ describe("BillingAccountProfileStateService", () => {
hasPremiumFromAnyOrganization: false, hasPremiumFromAnyOrganization: false,
}); });
expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); expect(await firstValueFrom(sut.hasPremiumFromAnySource$(userId))).toBe(true);
}); });
it("returns true when the user has premium from an organization", async () => { it("returns true when the user has premium from an organization", async () => {
@@ -106,7 +110,7 @@ describe("BillingAccountProfileStateService", () => {
hasPremiumFromAnyOrganization: true, hasPremiumFromAnyOrganization: true,
}); });
expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); expect(await firstValueFrom(sut.hasPremiumFromAnySource$(userId))).toBe(true);
}); });
it("returns true when they have premium personally AND from an organization", async () => { it("returns true when they have premium personally AND from an organization", async () => {
@@ -115,23 +119,87 @@ describe("BillingAccountProfileStateService", () => {
hasPremiumFromAnyOrganization: true, hasPremiumFromAnyOrganization: true,
}); });
expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); expect(await firstValueFrom(sut.hasPremiumFromAnySource$(userId))).toBe(true);
});
it("returns false when there is no active user", async () => {
await accountService.switchAccount(null);
expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(false);
}); });
}); });
describe("setHasPremium", () => { describe("setHasPremium", () => {
it("should update the active users state when called", async () => { it("should update the user's state when called", async () => {
await sut.setHasPremium(true, false, userId); await sut.setHasPremium(true, false, userId);
expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false); expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$(userId))).toBe(false);
expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true); expect(await firstValueFrom(sut.hasPremiumPersonally$(userId))).toBe(true);
expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); expect(await firstValueFrom(sut.hasPremiumFromAnySource$(userId))).toBe(true);
});
});
describe("canViewSubscription$", () => {
beforeEach(() => {
platformUtilsService.isSelfHost.mockReturnValue(false);
apiService.getUserBillingHistory.mockResolvedValue(
new BillingHistoryResponse({ invoices: [], transactions: [] }),
);
});
it("returns true when user has premium personally", async () => {
userBillingAccountProfileState.nextState({
hasPremiumPersonally: true,
hasPremiumFromAnyOrganization: true,
});
expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(true);
});
it("returns true when user has no premium from any source", async () => {
userBillingAccountProfileState.nextState({
hasPremiumPersonally: false,
hasPremiumFromAnyOrganization: false,
});
expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(true);
});
it("returns true when user has billing history in cloud environment", async () => {
userBillingAccountProfileState.nextState({
hasPremiumPersonally: false,
hasPremiumFromAnyOrganization: true,
});
platformUtilsService.isSelfHost.mockReturnValue(false);
apiService.getUserBillingHistory.mockResolvedValue(
new BillingHistoryResponse({
invoices: [{ id: "1" }],
transactions: [{ id: "2" }],
}),
);
expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(true);
});
it("returns false when user has no premium personally, has org premium, and no billing history", async () => {
userBillingAccountProfileState.nextState({
hasPremiumPersonally: false,
hasPremiumFromAnyOrganization: true,
});
platformUtilsService.isSelfHost.mockReturnValue(false);
apiService.getUserBillingHistory.mockResolvedValue(
new BillingHistoryResponse({
invoices: [],
transactions: [],
}),
);
expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(false);
});
it("returns false when user has no premium personally, has org premium, in self-hosted environment", async () => {
userBillingAccountProfileState.nextState({
hasPremiumPersonally: false,
hasPremiumFromAnyOrganization: true,
});
platformUtilsService.isSelfHost.mockReturnValue(true);
expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(false);
expect(apiService.getUserBillingHistory).not.toHaveBeenCalled();
}); });
}); });
}); });

View File

@@ -1,11 +1,9 @@
import { map, Observable, of, switchMap } from "rxjs"; import { map, Observable, combineLatest, concatMap } from "rxjs";
import { import { ApiService } from "@bitwarden/common/abstractions/api.service";
ActiveUserState, import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
BILLING_DISK,
StateProvider, import { BILLING_DISK, StateProvider, UserKeyDefinition } from "../../../platform/state";
UserKeyDefinition,
} from "../../../platform/state";
import { UserId } from "../../../types/guid"; import { UserId } from "../../../types/guid";
import { import {
BillingAccountProfile, BillingAccountProfile,
@@ -22,42 +20,34 @@ export const BILLING_ACCOUNT_PROFILE_KEY_DEFINITION = new UserKeyDefinition<Bill
); );
export class DefaultBillingAccountProfileStateService implements BillingAccountProfileStateService { export class DefaultBillingAccountProfileStateService implements BillingAccountProfileStateService {
private billingAccountProfileState: ActiveUserState<BillingAccountProfile>; constructor(
private readonly stateProvider: StateProvider,
private readonly platformUtilsService: PlatformUtilsService,
private readonly apiService: ApiService,
) {}
hasPremiumFromAnyOrganization$: Observable<boolean>; hasPremiumFromAnyOrganization$(userId: UserId): Observable<boolean> {
hasPremiumPersonally$: Observable<boolean>; return this.stateProvider
hasPremiumFromAnySource$: Observable<boolean>; .getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION)
.state$.pipe(map((profile) => !!profile?.hasPremiumFromAnyOrganization));
}
constructor(private readonly stateProvider: StateProvider) { hasPremiumPersonally$(userId: UserId): Observable<boolean> {
this.billingAccountProfileState = stateProvider.getActive( return this.stateProvider
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, .getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION)
); .state$.pipe(map((profile) => !!profile?.hasPremiumPersonally));
}
// Setup an observable that will always track the currently active user hasPremiumFromAnySource$(userId: UserId): Observable<boolean> {
// but will fallback to emitting null when there is no active user. return this.stateProvider
const billingAccountProfileOrNull = stateProvider.activeUserId$.pipe( .getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION)
switchMap((userId) => .state$.pipe(
userId != null map(
? stateProvider.getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION).state$ (profile) =>
: of(null), profile?.hasPremiumFromAnyOrganization === true ||
), profile?.hasPremiumPersonally === true,
); ),
);
this.hasPremiumFromAnyOrganization$ = billingAccountProfileOrNull.pipe(
map((billingAccountProfile) => !!billingAccountProfile?.hasPremiumFromAnyOrganization),
);
this.hasPremiumPersonally$ = billingAccountProfileOrNull.pipe(
map((billingAccountProfile) => !!billingAccountProfile?.hasPremiumPersonally),
);
this.hasPremiumFromAnySource$ = billingAccountProfileOrNull.pipe(
map(
(billingAccountProfile) =>
billingAccountProfile?.hasPremiumFromAnyOrganization === true ||
billingAccountProfile?.hasPremiumPersonally === true,
),
);
} }
async setHasPremium( async setHasPremium(
@@ -72,4 +62,23 @@ export class DefaultBillingAccountProfileStateService implements BillingAccountP
}; };
}); });
} }
canViewSubscription$(userId: UserId): Observable<boolean> {
return combineLatest([
this.hasPremiumPersonally$(userId),
this.hasPremiumFromAnyOrganization$(userId),
]).pipe(
concatMap(async ([hasPremiumPersonally, hasPremiumFromOrg]) => {
const isCloud = !this.platformUtilsService.isSelfHost();
let billing = null;
if (isCloud) {
billing = await this.apiService.getUserBillingHistory();
}
const cloudAndBillingHistory = isCloud && !billing?.hasNoHistory;
return hasPremiumPersonally || !hasPremiumFromOrg || cloudAndBillingHistory;
}),
);
}
} }

View File

@@ -4,6 +4,7 @@ import { Router, RouterLink } from "@angular/router";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { BadgeModule, ButtonModule, MenuModule } from "@bitwarden/components"; import { BadgeModule, ButtonModule, MenuModule } from "@bitwarden/components";
@@ -24,11 +25,18 @@ export class NewSendDropdownComponent implements OnInit {
constructor( constructor(
private router: Router, private router: Router,
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
private accountService: AccountService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
const account = await firstValueFrom(this.accountService.activeAccount$);
if (!account) {
this.hasNoPremium = true;
return;
}
this.hasNoPremium = !(await firstValueFrom( this.hasNoPremium = !(await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$, this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
)); ));
} }

View File

@@ -5,8 +5,10 @@ import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs"; import { of } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { UserId } from "@bitwarden/common/types/guid";
import { ChipSelectComponent } from "@bitwarden/components"; import { ChipSelectComponent } from "@bitwarden/components";
import { SendListFiltersService } from "../services/send-list-filters.service"; import { SendListFiltersService } from "../services/send-list-filters.service";
@@ -18,13 +20,22 @@ describe("SendListFiltersComponent", () => {
let fixture: ComponentFixture<SendListFiltersComponent>; let fixture: ComponentFixture<SendListFiltersComponent>;
let sendListFiltersService: SendListFiltersService; let sendListFiltersService: SendListFiltersService;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let accountService: MockProxy<AccountService>;
const userId = "userId" as UserId;
beforeEach(async () => { beforeEach(async () => {
sendListFiltersService = new SendListFiltersService(mock(), new FormBuilder()); sendListFiltersService = new SendListFiltersService(mock(), new FormBuilder());
sendListFiltersService.resetFilterForm = jest.fn(); sendListFiltersService.resetFilterForm = jest.fn();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
accountService = mock<AccountService>();
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); accountService.activeAccount$ = of({
id: userId,
email: "test@email.com",
emailVerified: true,
name: "Test User",
});
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ imports: [
@@ -37,10 +48,8 @@ describe("SendListFiltersComponent", () => {
providers: [ providers: [
{ provide: I18nService, useValue: { t: (key: string) => key } }, { provide: I18nService, useValue: { t: (key: string) => key } },
{ provide: SendListFiltersService, useValue: sendListFiltersService }, { provide: SendListFiltersService, useValue: sendListFiltersService },
{ { provide: BillingAccountProfileStateService, useValue: billingAccountProfileStateService },
provide: BillingAccountProfileStateService, { provide: AccountService, useValue: accountService },
useValue: billingAccountProfileStateService,
},
], ],
}).compileComponents(); }).compileComponents();
@@ -57,6 +66,7 @@ describe("SendListFiltersComponent", () => {
let canAccessPremium: boolean | undefined; let canAccessPremium: boolean | undefined;
component["canAccessPremium$"].subscribe((value) => (canAccessPremium = value)); component["canAccessPremium$"].subscribe((value) => (canAccessPremium = value));
expect(canAccessPremium).toBe(true); expect(canAccessPremium).toBe(true);
expect(billingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith(userId);
}); });
it("should call resetFilterForm on ngOnDestroy", () => { it("should call resetFilterForm on ngOnDestroy", () => {

View File

@@ -1,10 +1,11 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component, OnDestroy } from "@angular/core"; import { Component, OnDestroy } from "@angular/core";
import { ReactiveFormsModule } from "@angular/forms"; import { ReactiveFormsModule } from "@angular/forms";
import { Observable } from "rxjs"; import { Observable, of, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ChipSelectComponent } from "@bitwarden/components"; import { ChipSelectComponent } from "@bitwarden/components";
import { SendListFiltersService } from "../services/send-list-filters.service"; import { SendListFiltersService } from "../services/send-list-filters.service";
@@ -23,8 +24,15 @@ export class SendListFiltersComponent implements OnDestroy {
constructor( constructor(
private sendListFiltersService: SendListFiltersService, private sendListFiltersService: SendListFiltersService,
billingAccountProfileStateService: BillingAccountProfileStateService, billingAccountProfileStateService: BillingAccountProfileStateService,
accountService: AccountService,
) { ) {
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; this.canAccessPremium$ = accountService.activeAccount$.pipe(
switchMap((account) =>
account
? billingAccountProfileStateService.hasPremiumFromAnySource$(account.id)
: of(false),
),
);
} }
ngOnDestroy(): void { ngOnDestroy(): void {

View File

@@ -6,6 +6,7 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { NEVER, switchMap } from "rxjs"; import { NEVER, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { StateProvider } from "@bitwarden/common/platform/state"; import { StateProvider } from "@bitwarden/common/platform/state";
import { OrganizationId } from "@bitwarden/common/types/guid"; import { OrganizationId } from "@bitwarden/common/types/guid";
@@ -47,16 +48,22 @@ export class AttachmentsV2ViewComponent {
private keyService: KeyService, private keyService: KeyService,
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
private stateProvider: StateProvider, private stateProvider: StateProvider,
private accountService: AccountService,
) { ) {
this.subscribeToHasPremiumCheck(); this.subscribeToHasPremiumCheck();
this.subscribeToOrgKey(); this.subscribeToOrgKey();
} }
subscribeToHasPremiumCheck() { subscribeToHasPremiumCheck() {
this.billingAccountProfileStateService.hasPremiumFromAnySource$ this.accountService.activeAccount$
.pipe(takeUntilDestroyed()) .pipe(
.subscribe((data) => { switchMap((account) =>
this.canAccessPremium = data; this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
),
takeUntilDestroyed(),
)
.subscribe((hasPremium) => {
this.canAccessPremium = hasPremium;
}); });
} }

View File

@@ -6,10 +6,12 @@ import { BehaviorSubject } from "rxjs";
import { CopyClickDirective } from "@bitwarden/angular/directives/copy-click.directive"; import { CopyClickDirective } from "@bitwarden/angular/directives/copy-click.directive";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
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 { UserId } from "@bitwarden/common/types/guid";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -26,6 +28,17 @@ describe("LoginCredentialsViewComponent", () => {
let fixture: ComponentFixture<LoginCredentialsViewComponent>; let fixture: ComponentFixture<LoginCredentialsViewComponent>;
const hasPremiumFromAnySource$ = new BehaviorSubject<boolean>(true); const hasPremiumFromAnySource$ = new BehaviorSubject<boolean>(true);
const mockAccount = {
id: "test-user-id" as UserId,
email: "test@example.com",
emailVerified: true,
name: "Test User",
type: 0,
status: 0,
kdf: 0,
kdfIterations: 0,
};
const activeAccount$ = new BehaviorSubject(mockAccount);
const cipher = { const cipher = {
id: "cipher-id", id: "cipher-id",
@@ -48,8 +61,11 @@ describe("LoginCredentialsViewComponent", () => {
providers: [ providers: [
{ {
provide: BillingAccountProfileStateService, provide: BillingAccountProfileStateService,
useValue: mock<BillingAccountProfileStateService>({ hasPremiumFromAnySource$ }), useValue: mock<BillingAccountProfileStateService>({
hasPremiumFromAnySource$: () => hasPremiumFromAnySource$,
}),
}, },
{ provide: AccountService, useValue: mock<AccountService>({ activeAccount$ }) },
{ provide: PremiumUpgradePromptService, useValue: mock<PremiumUpgradePromptService>() }, { provide: PremiumUpgradePromptService, useValue: mock<PremiumUpgradePromptService>() },
{ provide: EventCollectionService, useValue: mock<EventCollectionService>({ collect }) }, { provide: EventCollectionService, useValue: mock<EventCollectionService>({ collect }) },
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() }, { provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },

View File

@@ -2,10 +2,11 @@
// @ts-strict-ignore // @ts-strict-ignore
import { CommonModule, DatePipe } from "@angular/common"; import { CommonModule, DatePipe } from "@angular/common";
import { Component, inject, Input } from "@angular/core"; import { Component, inject, Input } from "@angular/core";
import { Observable, shareReplay } from "rxjs"; import { Observable, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
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";
@@ -50,10 +51,11 @@ type TotpCodeValues = {
export class LoginCredentialsViewComponent { export class LoginCredentialsViewComponent {
@Input() cipher: CipherView; @Input() cipher: CipherView;
isPremium$: Observable<boolean> = isPremium$: Observable<boolean> = this.accountService.activeAccount$.pipe(
this.billingAccountProfileStateService.hasPremiumFromAnySource$.pipe( switchMap((account) =>
shareReplay({ refCount: true, bufferSize: 1 }), this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
); ),
);
showPasswordCount: boolean = false; showPasswordCount: boolean = false;
passwordRevealed: boolean = false; passwordRevealed: boolean = false;
totpCodeCopyObj: TotpCodeValues; totpCodeCopyObj: TotpCodeValues;
@@ -64,6 +66,7 @@ export class LoginCredentialsViewComponent {
private i18nService: I18nService, private i18nService: I18nService,
private premiumUpgradeService: PremiumUpgradePromptService, private premiumUpgradeService: PremiumUpgradePromptService,
private eventCollectionService: EventCollectionService, private eventCollectionService: EventCollectionService,
private accountService: AccountService,
) {} ) {}
get fido2CredentialCreationDateValue(): string { get fido2CredentialCreationDateValue(): string {

View File

@@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs"; import { of } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.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";
@@ -22,6 +23,8 @@ describe("CopyCipherFieldService", () => {
let totpService: MockProxy<TotpService>; let totpService: MockProxy<TotpService>;
let i18nService: MockProxy<I18nService>; let i18nService: MockProxy<I18nService>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let accountService: MockProxy<AccountService>;
const userId = "userId";
beforeEach(() => { beforeEach(() => {
platformUtilsService = mock<PlatformUtilsService>(); platformUtilsService = mock<PlatformUtilsService>();
@@ -31,6 +34,9 @@ describe("CopyCipherFieldService", () => {
totpService = mock<TotpService>(); totpService = mock<TotpService>();
i18nService = mock<I18nService>(); i18nService = mock<I18nService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
accountService = mock<AccountService>();
accountService.activeAccount$ = of({ id: userId } as Account);
service = new CopyCipherFieldService( service = new CopyCipherFieldService(
platformUtilsService, platformUtilsService,
@@ -40,6 +46,7 @@ describe("CopyCipherFieldService", () => {
totpService, totpService,
i18nService, i18nService,
billingAccountProfileStateService, billingAccountProfileStateService,
accountService,
); );
}); });
@@ -128,12 +135,15 @@ describe("CopyCipherFieldService", () => {
}); });
it("should get TOTP code when allowed from premium", async () => { it("should get TOTP code when allowed from premium", async () => {
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
totpService.getCode.mockResolvedValue("123456"); totpService.getCode.mockResolvedValue("123456");
const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt); const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt);
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(totpService.getCode).toHaveBeenCalledWith(valueToCopy); expect(totpService.getCode).toHaveBeenCalledWith(valueToCopy);
expect(platformUtilsService.copyToClipboard).toHaveBeenCalledWith("123456"); expect(platformUtilsService.copyToClipboard).toHaveBeenCalledWith("123456");
expect(billingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith(
userId,
);
}); });
it("should get TOTP code when allowed from organization", async () => { it("should get TOTP code when allowed from organization", async () => {
@@ -146,11 +156,14 @@ describe("CopyCipherFieldService", () => {
}); });
it("should return early when the user is not allowed to use TOTP", async () => { it("should return early when the user is not allowed to use TOTP", async () => {
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false); billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false));
const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt); const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt);
expect(result).toBeFalsy(); expect(result).toBeFalsy();
expect(totpService.getCode).not.toHaveBeenCalled(); expect(totpService.getCode).not.toHaveBeenCalled();
expect(platformUtilsService.copyToClipboard).not.toHaveBeenCalled(); expect(platformUtilsService.copyToClipboard).not.toHaveBeenCalled();
expect(billingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith(
userId,
);
}); });
it("should return early when TOTP is not set", async () => { it("should return early when TOTP is not set", async () => {

View File

@@ -2,6 +2,7 @@ import { Injectable } from "@angular/core";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.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";
@@ -87,6 +88,7 @@ export class CopyCipherFieldService {
private totpService: TotpService, private totpService: TotpService,
private i18nService: I18nService, private i18nService: I18nService,
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
private accountService: AccountService,
) {} ) {}
/** /**
@@ -148,10 +150,16 @@ export class CopyCipherFieldService {
* Determines if TOTP generation is allowed for a cipher and user. * Determines if TOTP generation is allowed for a cipher and user.
*/ */
async totpAllowed(cipher: CipherView): Promise<boolean> { async totpAllowed(cipher: CipherView): Promise<boolean> {
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
if (!activeAccount?.id) {
return false;
}
return ( return (
(cipher?.login?.hasTotp ?? false) && (cipher?.login?.hasTotp ?? false) &&
(cipher.organizationUseTotp || (cipher.organizationUseTotp ||
(await firstValueFrom(this.billingAccountProfileStateService.hasPremiumFromAnySource$))) (await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeAccount.id),
)))
); );
} }
} }