1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 05:43:41 +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:
Jonas Hendrickx
2025-04-18 15:57:27 +02:00
committed by GitHub
parent 9d16435d08
commit e026799071
14 changed files with 380 additions and 27 deletions

View File

@@ -100,20 +100,44 @@ describe("Organization Permissions Guard", () => {
it("permits navigation if the user has permissions", async () => {
const permissionsCallback = jest.fn();
permissionsCallback.mockImplementation((_org) => true);
permissionsCallback.mockReturnValue(true);
const actual = await TestBed.runInInjectionContext(
async () => await organizationPermissionsGuard(permissionsCallback)(route, state),
);
expect(permissionsCallback).toHaveBeenCalledWith(orgFactory({ id: targetOrgId }));
expect(permissionsCallback).toHaveBeenCalledTimes(1);
expect(actual).toBe(true);
});
it("handles a Promise returned from the callback", async () => {
const permissionsCallback = jest.fn();
permissionsCallback.mockReturnValue(Promise.resolve(true));
const actual = await TestBed.runInInjectionContext(() =>
organizationPermissionsGuard(permissionsCallback)(route, state),
);
expect(permissionsCallback).toHaveBeenCalledTimes(1);
expect(actual).toBe(true);
});
it("handles an Observable returned from the callback", async () => {
const permissionsCallback = jest.fn();
permissionsCallback.mockReturnValue(of(true));
const actual = await TestBed.runInInjectionContext(() =>
organizationPermissionsGuard(permissionsCallback)(route, state),
);
expect(permissionsCallback).toHaveBeenCalledTimes(1);
expect(actual).toBe(true);
});
describe("if the user does not have permissions", () => {
it("and there is no Item ID, block navigation", async () => {
const permissionsCallback = jest.fn();
permissionsCallback.mockImplementation((_org) => false);
permissionsCallback.mockReturnValue(false);
state = mock<RouterStateSnapshot>({
root: mock<ActivatedRouteSnapshot>({

View File

@@ -1,13 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { inject } from "@angular/core";
import { EnvironmentInjector, inject, runInInjectionContext } from "@angular/core";
import {
ActivatedRouteSnapshot,
CanActivateFn,
Router,
RouterStateSnapshot,
} from "@angular/router";
import { firstValueFrom, switchMap } from "rxjs";
import { firstValueFrom, isObservable, Observable, switchMap } from "rxjs";
import {
canAccessOrgAdmin,
@@ -42,7 +42,9 @@ import { ToastService } from "@bitwarden/components";
* proceeds as expected.
*/
export function organizationPermissionsGuard(
permissionsCallback?: (organization: Organization) => boolean,
permissionsCallback?: (
organization: Organization,
) => boolean | Promise<boolean> | Observable<boolean>,
): CanActivateFn {
return async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
const router = inject(Router);
@@ -51,6 +53,7 @@ export function organizationPermissionsGuard(
const i18nService = inject(I18nService);
const syncService = inject(SyncService);
const accountService = inject(AccountService);
const environmentInjector = inject(EnvironmentInjector);
// TODO: We need to fix issue once and for all.
if ((await syncService.getLastSync()) == null) {
@@ -78,7 +81,22 @@ export function organizationPermissionsGuard(
return router.createUrlTree(["/"]);
}
const hasPermissions = permissionsCallback == null || permissionsCallback(org);
if (permissionsCallback == null) {
// No additional permission checks required, allow navigation
return true;
}
const callbackResult = runInInjectionContext(environmentInjector, () =>
permissionsCallback(org),
);
const hasPermissions = isObservable(callbackResult)
? await firstValueFrom(callbackResult) // handles observables
: await Promise.resolve(callbackResult); // handles promises and boolean values
if (hasPermissions !== true && hasPermissions !== false) {
throw new Error("Permission callback did not resolve to a boolean.");
}
if (!hasPermissions) {
// Handle linkable ciphers for organizations the user only has view access to

View File

@@ -97,7 +97,7 @@
<bit-nav-item
[text]="'policies' | i18n"
route="settings/policies"
*ngIf="organization.canManagePolicies"
*ngIf="canShowPoliciesTab$ | async"
></bit-nav-item>
<bit-nav-item
[text]="'twoStepLogin' | i18n"

View File

@@ -22,6 +22,7 @@ import { PolicyType, ProviderStatusType } from "@bitwarden/common/admin-console/
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@@ -68,6 +69,7 @@ export class OrganizationLayoutComponent implements OnInit {
showAccountDeprovisioningBanner$: Observable<boolean>;
protected isBreadcrumbEventLogsEnabled$: Observable<boolean>;
protected showSponsoredFamiliesDropdown$: Observable<boolean>;
protected canShowPoliciesTab$: Observable<boolean>;
constructor(
private route: ActivatedRoute,
@@ -79,6 +81,7 @@ export class OrganizationLayoutComponent implements OnInit {
protected bannerService: AccountDeprovisioningBannerService,
private accountService: AccountService,
private freeFamiliesPolicyService: FreeFamiliesPolicyService,
private organizationBillingService: OrganizationBillingServiceAbstraction,
) {}
async ngOnInit() {
@@ -148,6 +151,18 @@ export class OrganizationLayoutComponent implements OnInit {
))
? "claimedDomains"
: "domainVerification";
this.canShowPoliciesTab$ = this.organization$.pipe(
switchMap((organization) =>
this.organizationBillingService
.isBreadcrumbingPoliciesEnabled$(organization)
.pipe(
map(
(isBreadcrumbingEnabled) => isBreadcrumbingEnabled || organization.canManagePolicies,
),
),
),
);
}
canShowVaultTab(organization: Organization): boolean {

View File

@@ -1,4 +1,17 @@
<app-header></app-header>
<app-header>
@let organization = organization$ | async;
<button
bitBadge
class="!tw-align-middle"
(click)="changePlan(organization)"
*ngIf="isBreadcrumbingEnabled$ | async"
slot="title-suffix"
type="button"
variant="primary"
>
{{ "upgrade" | i18n }}
</button>
</app-header>
<bit-container>
<ng-container *ngIf="loading">

View File

@@ -2,8 +2,8 @@
// @ts-strict-ignore
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { firstValueFrom, lastValueFrom } from "rxjs";
import { first, map } from "rxjs/operators";
import { firstValueFrom, lastValueFrom, map, Observable, switchMap } from "rxjs";
import { first } from "rxjs/operators";
import {
getOrganizationById,
@@ -14,10 +14,17 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { DialogService } from "@bitwarden/components";
import {
ChangePlanDialogResultType,
openChangePlanDialog,
} from "@bitwarden/web-vault/app/billing/organizations/change-plan-dialog.component";
import { All } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model";
import { PolicyListService } from "../../core/policy-list.service";
import { BasePolicy } from "../policies";
import { CollectionDialogTabType } from "../shared/components/collection-dialog";
import { PolicyEditComponent, PolicyEditDialogResult } from "./policy-edit.component";
@@ -32,17 +39,19 @@ export class PoliciesComponent implements OnInit {
loading = true;
organizationId: string;
policies: BasePolicy[];
organization: Organization;
protected organization$: Observable<Organization>;
private orgPolicies: PolicyResponse[];
protected policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
protected isBreadcrumbingEnabled$: Observable<boolean>;
constructor(
private route: ActivatedRoute,
private organizationService: OrganizationService,
private accountService: AccountService,
private organizationService: OrganizationService,
private policyApiService: PolicyApiServiceAbstraction,
private policyListService: PolicyListService,
private organizationBillingService: OrganizationBillingServiceAbstraction,
private dialogService: DialogService,
) {}
@@ -53,11 +62,9 @@ export class PoliciesComponent implements OnInit {
const userId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
this.organization = await firstValueFrom(
this.organizationService
this.organization$ = this.organizationService
.organizations$(userId)
.pipe(getOrganizationById(this.organizationId)),
);
.pipe(getOrganizationById(this.organizationId));
this.policies = this.policyListService.getPolicies();
await this.load();
@@ -91,7 +98,11 @@ export class PoliciesComponent implements OnInit {
this.orgPolicies.forEach((op) => {
this.policiesEnabledMap.set(op.type, op.enabled);
});
this.isBreadcrumbingEnabled$ = this.organization$.pipe(
switchMap((organization) =>
this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization),
),
);
this.loading = false;
}
@@ -104,8 +115,34 @@ export class PoliciesComponent implements OnInit {
});
const result = await lastValueFrom(dialogRef.closed);
if (result === PolicyEditDialogResult.Saved) {
switch (result) {
case PolicyEditDialogResult.Saved:
await this.load();
break;
case PolicyEditDialogResult.UpgradePlan:
await this.changePlan(await firstValueFrom(this.organization$));
break;
}
}
protected readonly CollectionDialogTabType = CollectionDialogTabType;
protected readonly All = All;
protected async changePlan(organization: Organization) {
const reference = openChangePlanDialog(this.dialogService, {
data: {
organizationId: organization.id,
subscription: null,
productTierType: organization.productTierType,
},
});
const result = await lastValueFrom(reference.closed);
if (result === ChangePlanDialogResultType.Closed) {
return;
}
await this.load();
}
}
}

View File

@@ -1,5 +1,17 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog [loading]="loading" [title]="'editPolicy' | i18n" [subtitle]="policy.name | i18n">
<ng-container bitDialogTitle>
<button
bitBadge
class="!tw-align-middle"
(click)="upgradePlan()"
*ngIf="isBreadcrumbingEnabled$ | async"
type="button"
variant="primary"
>
{{ "planNameEnterprise" | i18n }}
</button>
</ng-container>
<ng-container bitDialogContent>
<div *ngIf="loading">
<i
@@ -16,6 +28,7 @@
</ng-container>
<ng-container bitDialogFooter>
<button
*ngIf="!(isBreadcrumbingEnabled$ | async); else breadcrumbing"
bitButton
buttonType="primary"
[disabled]="saveDisabled$ | async"
@@ -24,6 +37,11 @@
>
{{ "save" | i18n }}
</button>
<ng-template #breadcrumbing>
<button bitButton buttonType="primary" bitFormButton type="button" (click)="upgradePlan()">
{{ "upgrade" | i18n }}
</button>
</ng-template>
<button bitButton buttonType="secondary" bitDialogClose type="button">
{{ "cancel" | i18n }}
</button>

View File

@@ -9,12 +9,20 @@ import {
ViewContainerRef,
} from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { Observable, map } from "rxjs";
import { map, Observable, switchMap } from "rxjs";
import {
getOrganizationById,
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import {
DIALOG_DATA,
@@ -35,6 +43,7 @@ export type PolicyEditDialogData = {
export enum PolicyEditDialogResult {
Saved = "saved",
UpgradePlan = "upgrade-plan",
}
@Component({
selector: "app-policy-edit",
@@ -48,22 +57,28 @@ export class PolicyEditComponent implements AfterViewInit {
loading = true;
enabled = false;
saveDisabled$: Observable<boolean>;
defaultTypes: any[];
policyComponent: BasePolicyComponent;
private policyResponse: PolicyResponse;
formGroup = this.formBuilder.group({
enabled: [this.enabled],
});
protected organization$: Observable<Organization>;
protected isBreadcrumbingEnabled$: Observable<boolean>;
constructor(
@Inject(DIALOG_DATA) protected data: PolicyEditDialogData,
private accountService: AccountService,
private policyApiService: PolicyApiServiceAbstraction,
private organizationService: OrganizationService,
private i18nService: I18nService,
private cdr: ChangeDetectorRef,
private formBuilder: FormBuilder,
private dialogRef: DialogRef<PolicyEditDialogResult>,
private toastService: ToastService,
private organizationBillingService: OrganizationBillingServiceAbstraction,
) {}
get policy(): BasePolicy {
return this.data.policy;
}
@@ -97,6 +112,16 @@ export class PolicyEditComponent implements AfterViewInit {
throw e;
}
}
this.organization$ = this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.organizationService.organizations$(userId)),
getOrganizationById(this.data.organizationId),
);
this.isBreadcrumbingEnabled$ = this.organization$.pipe(
switchMap((organization) =>
this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization),
),
);
}
submit = async () => {
@@ -119,4 +144,8 @@ export class PolicyEditComponent implements AfterViewInit {
static open = (dialogService: DialogService, config: DialogConfig<PolicyEditDialogData>) => {
return dialogService.open<PolicyEditDialogResult>(PolicyEditComponent, config);
};
protected upgradePlan(): void {
this.dialogRef.close(PolicyEditDialogResult.UpgradePlan);
}
}

View File

@@ -1,8 +1,10 @@
import { NgModule } from "@angular/core";
import { NgModule, inject } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { map } from "rxjs";
import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { organizationPermissionsGuard } from "../../organizations/guards/org-permissions.guard";
import { organizationRedirectGuard } from "../../organizations/guards/org-redirect.guard";
@@ -41,7 +43,14 @@ const routes: Routes = [
{
path: "policies",
component: PoliciesComponent,
canActivate: [organizationPermissionsGuard((org) => org.canManagePolicies)],
canActivate: [
organizationPermissionsGuard((o: Organization) => {
const organizationBillingService = inject(OrganizationBillingServiceAbstraction);
return organizationBillingService
.isBreadcrumbingPoliciesEnabled$(o)
.pipe(map((isBreadcrumbingEnabled) => o.canManagePolicies || isBreadcrumbingEnabled));
}),
],
data: {
titleId: "policies",
},

View File

@@ -12,7 +12,7 @@
<h1
bitTypography="h1"
noMargin
class="tw-m-0 tw-mr-2 tw-leading-10 tw-flex"
class="tw-m-0 tw-mr-2 tw-leading-10 tw-flex tw-gap-1"
[title]="title || (routeData.titleId | i18n)"
>
<div class="tw-truncate">

View File

@@ -1255,6 +1255,7 @@ const safeProviders: SafeProvider[] = [
I18nServiceAbstraction,
OrganizationApiServiceAbstraction,
SyncService,
ConfigService,
],
}),
safeProvider({

View File

@@ -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>;
}

View File

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

View File

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