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:
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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>() },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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$();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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"]);
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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$,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>() },
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
)))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user