mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 17:23:37 +00:00
[PM-13128] Enable Breadcrumb Policies (#13584)
* [PM-13128] Enable Breadcrumb Policies * [PM-13128] Enable Breadcrumb Policies * [PM-13128] wip * [PM-13128] wip * [PM-13128] wip * [PM-13128] wip * remove dead code * wip * wip * wip * refactor * Fix for providers * revert to functional auth guard * change prerequisite to info variant * address comment * r * r * r * tests * r * r * fix tests * feedback * fix tests * fix tests * Rename upselling to breadcrumbing * Address feedback * Fix build & tests * Make the guard callback use Observable instead of a promise * Pm 13128 suggestions (#14041) * Rename new enum value * Show the upgrade button when breadcrumbing is enabled * Show mouse pointer when cursor is hovered above badge * Do not make the dialogs overlap * Align badge middle * Gap * Badge should be a `button` instead of `span` * missing button@type --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Alex Morask <amorask@bitwarden.com>
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
|
||||
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
|
||||
import { InitiationPath } from "../../models/request/reference-event.request";
|
||||
@@ -59,4 +62,10 @@ export abstract class OrganizationBillingServiceAbstraction {
|
||||
organizationId: string,
|
||||
subscription: SubscriptionInformation,
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Determines if breadcrumbing policies is enabled for the organizations meeting certain criteria.
|
||||
* @param organization
|
||||
*/
|
||||
abstract isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable<boolean>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction as OrganizationApiService } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
describe("BillingAccountProfileStateService", () => {
|
||||
let apiService: jest.Mocked<ApiService>;
|
||||
let billingApiService: jest.Mocked<BillingApiServiceAbstraction>;
|
||||
let keyService: jest.Mocked<KeyService>;
|
||||
let encryptService: jest.Mocked<EncryptService>;
|
||||
let i18nService: jest.Mocked<I18nService>;
|
||||
let organizationApiService: jest.Mocked<OrganizationApiService>;
|
||||
let syncService: jest.Mocked<SyncService>;
|
||||
let configService: jest.Mocked<ConfigService>;
|
||||
|
||||
let sut: OrganizationBillingService;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
billingApiService = mock<BillingApiServiceAbstraction>();
|
||||
keyService = mock<KeyService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
i18nService = mock<I18nService>();
|
||||
organizationApiService = mock<OrganizationApiService>();
|
||||
syncService = mock<SyncService>();
|
||||
configService = mock<ConfigService>();
|
||||
|
||||
sut = new OrganizationBillingService(
|
||||
apiService,
|
||||
billingApiService,
|
||||
keyService,
|
||||
encryptService,
|
||||
i18nService,
|
||||
organizationApiService,
|
||||
syncService,
|
||||
configService,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("isBreadcrumbingPoliciesEnabled", () => {
|
||||
it("returns false when feature flag is disabled", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
const org = {
|
||||
isProviderUser: false,
|
||||
canEditSubscription: true,
|
||||
productTierType: ProductTierType.Teams,
|
||||
} as Organization;
|
||||
|
||||
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
||||
expect(actual).toBe(false);
|
||||
expect(configService.getFeatureFlag$).toHaveBeenCalledWith(
|
||||
FeatureFlag.PM12276_BreadcrumbEventLogs,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false when organization belongs to a provider", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
const org = {
|
||||
isProviderUser: true,
|
||||
canEditSubscription: true,
|
||||
productTierType: ProductTierType.Teams,
|
||||
} as Organization;
|
||||
|
||||
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
||||
expect(actual).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when cannot edit subscription", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
const org = {
|
||||
isProviderUser: false,
|
||||
canEditSubscription: false,
|
||||
productTierType: ProductTierType.Teams,
|
||||
} as Organization;
|
||||
|
||||
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
||||
expect(actual).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["Teams", ProductTierType.Teams],
|
||||
["TeamsStarter", ProductTierType.TeamsStarter],
|
||||
])("returns true when all conditions are met with %s tier", async (_, productTierType) => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
const org = {
|
||||
isProviderUser: false,
|
||||
canEditSubscription: true,
|
||||
productTierType: productTierType,
|
||||
} as Organization;
|
||||
|
||||
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
||||
expect(actual).toBe(true);
|
||||
expect(configService.getFeatureFlag$).toHaveBeenCalledWith(
|
||||
FeatureFlag.PM12276_BreadcrumbEventLogs,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false when product tier is not supported", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
const org = {
|
||||
isProviderUser: false,
|
||||
canEditSubscription: true,
|
||||
productTierType: ProductTierType.Enterprise,
|
||||
} as Organization;
|
||||
|
||||
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
||||
expect(actual).toBe(false);
|
||||
});
|
||||
|
||||
it("handles all conditions false correctly", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
const org = {
|
||||
isProviderUser: true,
|
||||
canEditSubscription: false,
|
||||
productTierType: ProductTierType.Free,
|
||||
} as Organization;
|
||||
|
||||
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
||||
expect(actual).toBe(false);
|
||||
});
|
||||
|
||||
it("verifies feature flag is only called once", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
const org = {
|
||||
isProviderUser: false,
|
||||
canEditSubscription: true,
|
||||
productTierType: ProductTierType.Teams,
|
||||
} as Organization;
|
||||
|
||||
await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
||||
expect(configService.getFeatureFlag$).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,10 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable, of, switchMap } from "rxjs";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
@@ -20,7 +25,7 @@ import {
|
||||
PlanInformation,
|
||||
SubscriptionInformation,
|
||||
} from "../abstractions";
|
||||
import { PlanType } from "../enums";
|
||||
import { PlanType, ProductTierType } from "../enums";
|
||||
import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request";
|
||||
import { PaymentSourceResponse } from "../models/response/payment-source.response";
|
||||
|
||||
@@ -40,6 +45,7 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
|
||||
private i18nService: I18nService,
|
||||
private organizationApiService: OrganizationApiService,
|
||||
private syncService: SyncService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async getPaymentSource(organizationId: string): Promise<PaymentSourceResponse> {
|
||||
@@ -220,4 +226,29 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
|
||||
this.setPaymentInformation(request, subscription.payment);
|
||||
await this.billingApiService.restartSubscription(organizationId, request);
|
||||
}
|
||||
|
||||
isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable<boolean> {
|
||||
if (organization === null || organization === undefined) {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
return this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs).pipe(
|
||||
switchMap((featureFlagEnabled) => {
|
||||
if (!featureFlagEnabled) {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
if (organization.isProviderUser || !organization.canEditSubscription) {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
const supportedProducts = [ProductTierType.Teams, ProductTierType.TeamsStarter];
|
||||
const isSupportedProduct = supportedProducts.some(
|
||||
(product) => product === organization.productTierType,
|
||||
);
|
||||
|
||||
return of(isSupportedProduct);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user