mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
[PM-18955] Use OrganizationWarningsService on AC Collections Page behind FF (#14437)
* Add getWarnings to OrganizationBillingApiService * Add OrganizationWarningsService * Add feature flag * Add standalone warning components that consume new service * Add new components to AC collections vault when FF is enabled * Add OrganizationWarningsService spec * Run prettier on spec file * Thomas' feedback
This commit is contained in:
@@ -1,4 +1,15 @@
|
|||||||
<ng-container *ngIf="freeTrial$ | async as freeTrial">
|
<app-free-trial-warning
|
||||||
|
*ngIf="useOrganizationWarningsService$ | async"
|
||||||
|
[organization]="organization"
|
||||||
|
(clicked)="navigateToPaymentMethod()"
|
||||||
|
>
|
||||||
|
</app-free-trial-warning>
|
||||||
|
<app-reseller-renewal-warning
|
||||||
|
*ngIf="useOrganizationWarningsService$ | async"
|
||||||
|
[organization]="organization"
|
||||||
|
>
|
||||||
|
</app-reseller-renewal-warning>
|
||||||
|
<ng-container *ngIf="freeTrialWhenWarningsServiceDisabled$ | async as freeTrial">
|
||||||
<bit-banner
|
<bit-banner
|
||||||
id="free-trial-banner"
|
id="free-trial-banner"
|
||||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||||
@@ -19,7 +30,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</bit-banner>
|
</bit-banner>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="resellerWarning$ | async as resellerWarning">
|
<ng-container *ngIf="resellerWarningWhenWarningsServiceDisabled$ | async as resellerWarning">
|
||||||
<bit-banner
|
<bit-banner
|
||||||
id="reseller-warning-banner"
|
id="reseller-warning-banner"
|
||||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Subject,
|
Subject,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
import {
|
import {
|
||||||
|
catchError,
|
||||||
concatMap,
|
concatMap,
|
||||||
debounceTime,
|
debounceTime,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
@@ -23,7 +24,6 @@ import {
|
|||||||
switchMap,
|
switchMap,
|
||||||
takeUntil,
|
takeUntil,
|
||||||
tap,
|
tap,
|
||||||
catchError,
|
|
||||||
} from "rxjs/operators";
|
} from "rxjs/operators";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -44,6 +44,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
|||||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||||
import { EventType } from "@bitwarden/common/enums";
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@@ -61,8 +62,8 @@ import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
|||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||||
import {
|
import {
|
||||||
DialogRef,
|
|
||||||
BannerModule,
|
BannerModule,
|
||||||
|
DialogRef,
|
||||||
DialogService,
|
DialogService,
|
||||||
Icons,
|
Icons,
|
||||||
NoItemsModule,
|
NoItemsModule,
|
||||||
@@ -77,6 +78,8 @@ import {
|
|||||||
DecryptionFailureDialogComponent,
|
DecryptionFailureDialogComponent,
|
||||||
PasswordRepromptService,
|
PasswordRepromptService,
|
||||||
} from "@bitwarden/vault";
|
} from "@bitwarden/vault";
|
||||||
|
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/services/organization-warnings.service";
|
||||||
|
import { ResellerRenewalWarningComponent } from "@bitwarden/web-vault/app/billing/warnings/reseller-renewal-warning.component";
|
||||||
|
|
||||||
import { BillingNotificationService } from "../../../billing/services/billing-notification.service";
|
import { BillingNotificationService } from "../../../billing/services/billing-notification.service";
|
||||||
import {
|
import {
|
||||||
@@ -85,6 +88,7 @@ import {
|
|||||||
} from "../../../billing/services/reseller-warning.service";
|
} from "../../../billing/services/reseller-warning.service";
|
||||||
import { TrialFlowService } from "../../../billing/services/trial-flow.service";
|
import { TrialFlowService } from "../../../billing/services/trial-flow.service";
|
||||||
import { FreeTrial } from "../../../billing/types/free-trial";
|
import { FreeTrial } from "../../../billing/types/free-trial";
|
||||||
|
import { FreeTrialWarningComponent } from "../../../billing/warnings/free-trial-warning.component";
|
||||||
import { SharedModule } from "../../../shared";
|
import { SharedModule } from "../../../shared";
|
||||||
import { AssignCollectionsWebComponent } from "../../../vault/components/assign-collections";
|
import { AssignCollectionsWebComponent } from "../../../vault/components/assign-collections";
|
||||||
import {
|
import {
|
||||||
@@ -145,6 +149,8 @@ enum AddAccessStatusType {
|
|||||||
SharedModule,
|
SharedModule,
|
||||||
BannerModule,
|
BannerModule,
|
||||||
NoItemsModule,
|
NoItemsModule,
|
||||||
|
FreeTrialWarningComponent,
|
||||||
|
ResellerRenewalWarningComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
RoutedVaultFilterService,
|
RoutedVaultFilterService,
|
||||||
@@ -174,8 +180,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
protected showCollectionAccessRestricted: boolean;
|
protected showCollectionAccessRestricted: boolean;
|
||||||
private hasSubscription$ = new BehaviorSubject<boolean>(false);
|
private hasSubscription$ = new BehaviorSubject<boolean>(false);
|
||||||
protected currentSearchText$: Observable<string>;
|
protected currentSearchText$: Observable<string>;
|
||||||
protected freeTrial$: Observable<FreeTrial>;
|
protected useOrganizationWarningsService$: Observable<boolean>;
|
||||||
protected resellerWarning$: Observable<ResellerWarning | null>;
|
protected freeTrialWhenWarningsServiceDisabled$: Observable<FreeTrial>;
|
||||||
|
protected resellerWarningWhenWarningsServiceDisabled$: Observable<ResellerWarning | null>;
|
||||||
protected prevCipherId: string | null = null;
|
protected prevCipherId: string | null = null;
|
||||||
protected userId: UserId;
|
protected userId: UserId;
|
||||||
/**
|
/**
|
||||||
@@ -255,6 +262,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private resellerWarningService: ResellerWarningService,
|
private resellerWarningService: ResellerWarningService,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private billingNotificationService: BillingNotificationService,
|
private billingNotificationService: BillingNotificationService,
|
||||||
|
private organizationWarningsService: OrganizationWarningsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -628,9 +636,23 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
)
|
)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
|
|
||||||
this.unpaidSubscriptionDialog$.pipe(takeUntil(this.destroy$)).subscribe();
|
// Billing Warnings
|
||||||
|
this.useOrganizationWarningsService$ = this.configService.getFeatureFlag$(
|
||||||
|
FeatureFlag.UseOrganizationWarningsService,
|
||||||
|
);
|
||||||
|
|
||||||
this.freeTrial$ = combineLatest([
|
this.useOrganizationWarningsService$
|
||||||
|
.pipe(
|
||||||
|
switchMap((enabled) =>
|
||||||
|
enabled
|
||||||
|
? this.organizationWarningsService.showInactiveSubscriptionDialog$(this.organization)
|
||||||
|
: this.unpaidSubscriptionDialog$,
|
||||||
|
),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
const freeTrial$ = combineLatest([
|
||||||
organization$,
|
organization$,
|
||||||
this.hasSubscription$.pipe(filter((hasSubscription) => hasSubscription !== null)),
|
this.hasSubscription$.pipe(filter((hasSubscription) => hasSubscription !== null)),
|
||||||
]).pipe(
|
]).pipe(
|
||||||
@@ -655,7 +677,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
filter((result) => result !== null),
|
filter((result) => result !== null),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.resellerWarning$ = organization$.pipe(
|
this.freeTrialWhenWarningsServiceDisabled$ = this.useOrganizationWarningsService$.pipe(
|
||||||
|
filter((enabled) => !enabled),
|
||||||
|
switchMap(() => freeTrial$),
|
||||||
|
);
|
||||||
|
|
||||||
|
const resellerWarning$ = organization$.pipe(
|
||||||
filter((org) => org.isOwner),
|
filter((org) => org.isOwner),
|
||||||
switchMap((org) =>
|
switchMap((org) =>
|
||||||
from(this.billingApiService.getOrganizationBillingMetadata(org.id)).pipe(
|
from(this.billingApiService.getOrganizationBillingMetadata(org.id)).pipe(
|
||||||
@@ -665,6 +692,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
map(({ org, metadata }) => this.resellerWarningService.getWarning(org, metadata)),
|
map(({ org, metadata }) => this.resellerWarningService.getWarning(org, metadata)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.resellerWarningWhenWarningsServiceDisabled$ = this.useOrganizationWarningsService$.pipe(
|
||||||
|
filter((enabled) => !enabled),
|
||||||
|
switchMap(() => resellerWarning$),
|
||||||
|
);
|
||||||
|
// End Billing Warnings
|
||||||
|
|
||||||
firstSetup$
|
firstSetup$
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(() => this.refresh$),
|
switchMap(() => this.refresh$),
|
||||||
|
|||||||
@@ -0,0 +1,356 @@
|
|||||||
|
import { Router } from "@angular/router";
|
||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { firstValueFrom, lastValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction";
|
||||||
|
import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.response";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { OrganizationWarningsService } from "./organization-warnings.service";
|
||||||
|
|
||||||
|
describe("OrganizationWarningsService", () => {
|
||||||
|
let dialogService: MockProxy<DialogService>;
|
||||||
|
let i18nService: MockProxy<I18nService>;
|
||||||
|
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
|
||||||
|
let organizationBillingApiService: MockProxy<OrganizationBillingApiServiceAbstraction>;
|
||||||
|
let router: MockProxy<Router>;
|
||||||
|
|
||||||
|
let organizationWarningsService: OrganizationWarningsService;
|
||||||
|
|
||||||
|
const respond = (responseBody: any) =>
|
||||||
|
Promise.resolve(new OrganizationWarningsResponse(responseBody));
|
||||||
|
|
||||||
|
const empty = () => Promise.resolve(new OrganizationWarningsResponse({}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
dialogService = mock<DialogService>();
|
||||||
|
i18nService = mock<I18nService>();
|
||||||
|
organizationApiService = mock<OrganizationApiServiceAbstraction>();
|
||||||
|
organizationBillingApiService = mock<OrganizationBillingApiServiceAbstraction>();
|
||||||
|
router = mock<Router>();
|
||||||
|
|
||||||
|
organizationWarningsService = new OrganizationWarningsService(
|
||||||
|
dialogService,
|
||||||
|
i18nService,
|
||||||
|
organizationApiService,
|
||||||
|
organizationBillingApiService,
|
||||||
|
router,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cache$", () => {
|
||||||
|
it("should only request warnings once for a specific organization and replay the cached result for multiple subscriptions", async () => {
|
||||||
|
const response1 = respond({
|
||||||
|
freeTrial: {
|
||||||
|
remainingTrialDays: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const organization1 = {
|
||||||
|
id: "1",
|
||||||
|
name: "Test",
|
||||||
|
} as Organization;
|
||||||
|
|
||||||
|
const response2 = respond({
|
||||||
|
freeTrial: {
|
||||||
|
remainingTrialDays: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const organization2 = {
|
||||||
|
id: "2",
|
||||||
|
name: "Test",
|
||||||
|
} as Organization;
|
||||||
|
|
||||||
|
organizationBillingApiService.getWarnings.mockImplementation((id) => {
|
||||||
|
if (id === organization1.id) {
|
||||||
|
return response1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id === organization2.id) {
|
||||||
|
return response2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return empty();
|
||||||
|
});
|
||||||
|
|
||||||
|
const oneDayRemainingTranslation = "oneDayRemaining";
|
||||||
|
const twoDaysRemainingTranslation = "twoDaysRemaining";
|
||||||
|
|
||||||
|
i18nService.t.mockImplementation((id, p1) => {
|
||||||
|
if (id === "freeTrialEndPromptTomorrowNoOrgName") {
|
||||||
|
return oneDayRemainingTranslation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id === "freeTrialEndPromptCount" && p1 === 2) {
|
||||||
|
return twoDaysRemainingTranslation;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
|
const organization1Subscription1 = await firstValueFrom(
|
||||||
|
organizationWarningsService.getFreeTrialWarning$(organization1),
|
||||||
|
);
|
||||||
|
|
||||||
|
const organization1Subscription2 = await firstValueFrom(
|
||||||
|
organizationWarningsService.getFreeTrialWarning$(organization1),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(organization1Subscription1).toEqual({
|
||||||
|
organization: organization1,
|
||||||
|
message: oneDayRemainingTranslation,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(organization1Subscription2).toEqual(organization1Subscription1);
|
||||||
|
|
||||||
|
const organization2Subscription1 = await firstValueFrom(
|
||||||
|
organizationWarningsService.getFreeTrialWarning$(organization2),
|
||||||
|
);
|
||||||
|
|
||||||
|
const organization2Subscription2 = await firstValueFrom(
|
||||||
|
organizationWarningsService.getFreeTrialWarning$(organization2),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(organization2Subscription1).toEqual({
|
||||||
|
organization: organization2,
|
||||||
|
message: twoDaysRemainingTranslation,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(organization2Subscription2).toEqual(organization2Subscription1);
|
||||||
|
|
||||||
|
expect(organizationBillingApiService.getWarnings).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getFreeTrialWarning$", () => {
|
||||||
|
it("should not emit a free trial warning when none is included in the warnings response", (done) => {
|
||||||
|
const organization = {
|
||||||
|
id: "1",
|
||||||
|
name: "Test",
|
||||||
|
} as Organization;
|
||||||
|
|
||||||
|
organizationBillingApiService.getWarnings.mockReturnValue(empty());
|
||||||
|
|
||||||
|
const warning$ = organizationWarningsService.getFreeTrialWarning$(organization);
|
||||||
|
|
||||||
|
warning$.subscribe({
|
||||||
|
next: () => {
|
||||||
|
fail("Observable should not emit a value.");
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should emit a free trial warning when one is included in the warnings response", async () => {
|
||||||
|
const response = respond({
|
||||||
|
freeTrial: {
|
||||||
|
remainingTrialDays: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const organization = {
|
||||||
|
id: "1",
|
||||||
|
name: "Test",
|
||||||
|
} as Organization;
|
||||||
|
|
||||||
|
organizationBillingApiService.getWarnings.mockImplementation((id) => {
|
||||||
|
if (id === organization.id) {
|
||||||
|
return response;
|
||||||
|
} else {
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const translation = "translation";
|
||||||
|
i18nService.t.mockImplementation((id) => {
|
||||||
|
if (id === "freeTrialEndPromptTomorrowNoOrgName") {
|
||||||
|
return translation;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const warning = await firstValueFrom(
|
||||||
|
organizationWarningsService.getFreeTrialWarning$(organization),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(warning).toEqual({
|
||||||
|
organization,
|
||||||
|
message: translation,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getResellerRenewalWarning$", () => {
|
||||||
|
it("should not emit a reseller renewal warning when none is included in the warnings response", (done) => {
|
||||||
|
const organization = {
|
||||||
|
id: "1",
|
||||||
|
name: "Test",
|
||||||
|
} as Organization;
|
||||||
|
|
||||||
|
organizationBillingApiService.getWarnings.mockReturnValue(empty());
|
||||||
|
|
||||||
|
const warning$ = organizationWarningsService.getResellerRenewalWarning$(organization);
|
||||||
|
|
||||||
|
warning$.subscribe({
|
||||||
|
next: () => {
|
||||||
|
fail("Observable should not emit a value.");
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should emit a reseller renewal warning when one is included in the warnings response", async () => {
|
||||||
|
const response = respond({
|
||||||
|
resellerRenewal: {
|
||||||
|
type: "upcoming",
|
||||||
|
upcoming: {
|
||||||
|
renewalDate: "2026-01-01T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const organization = {
|
||||||
|
id: "1",
|
||||||
|
name: "Test",
|
||||||
|
providerName: "Provider",
|
||||||
|
} as Organization;
|
||||||
|
|
||||||
|
organizationBillingApiService.getWarnings.mockImplementation((id) => {
|
||||||
|
if (id === organization.id) {
|
||||||
|
return response;
|
||||||
|
} else {
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedDate = new Date("2026-01-01T00:00:00.000Z").toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
|
||||||
|
const translation = "translation";
|
||||||
|
i18nService.t.mockImplementation((id, p1, p2) => {
|
||||||
|
if (
|
||||||
|
id === "resellerRenewalWarningMsg" &&
|
||||||
|
p1 === organization.providerName &&
|
||||||
|
p2 === formattedDate
|
||||||
|
) {
|
||||||
|
return translation;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const warning = await firstValueFrom(
|
||||||
|
organizationWarningsService.getResellerRenewalWarning$(organization),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(warning).toEqual({
|
||||||
|
type: "info",
|
||||||
|
message: translation,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("showInactiveSubscriptionDialog$", () => {
|
||||||
|
it("should not emit the opening of a dialog for an inactive subscription warning when the warning is not included in the warnings response", (done) => {
|
||||||
|
const organization = {
|
||||||
|
id: "1",
|
||||||
|
name: "Test",
|
||||||
|
} as Organization;
|
||||||
|
|
||||||
|
organizationBillingApiService.getWarnings.mockReturnValue(empty());
|
||||||
|
|
||||||
|
const warning$ = organizationWarningsService.showInactiveSubscriptionDialog$(organization);
|
||||||
|
|
||||||
|
warning$.subscribe({
|
||||||
|
next: () => {
|
||||||
|
fail("Observable should not emit a value.");
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should emit the opening of a dialog for an inactive subscription warning when the warning is included in the warnings response", async () => {
|
||||||
|
const response = respond({
|
||||||
|
inactiveSubscription: {
|
||||||
|
resolution: "add_payment_method",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const organization = {
|
||||||
|
id: "1",
|
||||||
|
name: "Test",
|
||||||
|
providerName: "Provider",
|
||||||
|
} as Organization;
|
||||||
|
|
||||||
|
organizationBillingApiService.getWarnings.mockImplementation((id) => {
|
||||||
|
if (id === organization.id) {
|
||||||
|
return response;
|
||||||
|
} else {
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const titleTranslation = "title";
|
||||||
|
const continueTranslation = "continue";
|
||||||
|
const closeTranslation = "close";
|
||||||
|
|
||||||
|
i18nService.t.mockImplementation((id, param) => {
|
||||||
|
if (id === "suspendedOrganizationTitle" && param === organization.name) {
|
||||||
|
return titleTranslation;
|
||||||
|
}
|
||||||
|
if (id === "continue") {
|
||||||
|
return continueTranslation;
|
||||||
|
}
|
||||||
|
if (id === "close") {
|
||||||
|
return closeTranslation;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedOptions = {
|
||||||
|
title: titleTranslation,
|
||||||
|
content: {
|
||||||
|
key: "suspendedOwnerOrgMessage",
|
||||||
|
},
|
||||||
|
type: "danger",
|
||||||
|
acceptButtonText: continueTranslation,
|
||||||
|
cancelButtonText: closeTranslation,
|
||||||
|
} as SimpleDialogOptions;
|
||||||
|
|
||||||
|
dialogService.openSimpleDialog.mockImplementation((options) => {
|
||||||
|
if (JSON.stringify(options) == JSON.stringify(expectedOptions)) {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const observable$ = organizationWarningsService.showInactiveSubscriptionDialog$(organization);
|
||||||
|
|
||||||
|
const routerNavigateSpy = jest.spyOn(router, "navigate").mockResolvedValue(true);
|
||||||
|
|
||||||
|
await lastValueFrom(observable$);
|
||||||
|
|
||||||
|
expect(routerNavigateSpy).toHaveBeenCalledWith(
|
||||||
|
["organizations", `${organization.id}`, "billing", "payment-method"],
|
||||||
|
{
|
||||||
|
state: { launchPaymentModalAutomatically: true },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
import {
|
||||||
|
filter,
|
||||||
|
from,
|
||||||
|
lastValueFrom,
|
||||||
|
map,
|
||||||
|
Observable,
|
||||||
|
shareReplay,
|
||||||
|
switchMap,
|
||||||
|
takeWhile,
|
||||||
|
} from "rxjs";
|
||||||
|
import { take } from "rxjs/operators";
|
||||||
|
|
||||||
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction";
|
||||||
|
import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.response";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
import { openChangePlanDialog } from "@bitwarden/web-vault/app/billing/organizations/change-plan-dialog.component";
|
||||||
|
|
||||||
|
const format = (date: Date) =>
|
||||||
|
date.toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FreeTrialWarning = {
|
||||||
|
organization: Pick<Organization, "id" & "name">;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResellerRenewalWarning = {
|
||||||
|
type: "info" | "warning";
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable({ providedIn: "root" })
|
||||||
|
export class OrganizationWarningsService {
|
||||||
|
private cache$ = new Map<OrganizationId, Observable<OrganizationWarningsResponse>>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
|
private organizationBillingApiService: OrganizationBillingApiServiceAbstraction,
|
||||||
|
private router: Router,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getFreeTrialWarning$ = (organization: Organization): Observable<FreeTrialWarning> =>
|
||||||
|
this.getWarning$(organization, (response) => response.freeTrial).pipe(
|
||||||
|
map((warning) => {
|
||||||
|
const { remainingTrialDays } = warning;
|
||||||
|
|
||||||
|
if (remainingTrialDays >= 2) {
|
||||||
|
return {
|
||||||
|
organization,
|
||||||
|
message: this.i18nService.t("freeTrialEndPromptCount", remainingTrialDays),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingTrialDays == 1) {
|
||||||
|
return {
|
||||||
|
organization,
|
||||||
|
message: this.i18nService.t("freeTrialEndPromptTomorrowNoOrgName"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
organization,
|
||||||
|
message: this.i18nService.t("freeTrialEndingTodayWithoutOrgName"),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
getResellerRenewalWarning$ = (organization: Organization): Observable<ResellerRenewalWarning> =>
|
||||||
|
this.getWarning$(organization, (response) => response.resellerRenewal).pipe(
|
||||||
|
map((warning): ResellerRenewalWarning | null => {
|
||||||
|
switch (warning.type) {
|
||||||
|
case "upcoming": {
|
||||||
|
return {
|
||||||
|
type: "info",
|
||||||
|
message: this.i18nService.t(
|
||||||
|
"resellerRenewalWarningMsg",
|
||||||
|
organization.providerName,
|
||||||
|
format(warning.upcoming!.renewalDate),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "issued": {
|
||||||
|
return {
|
||||||
|
type: "info",
|
||||||
|
message: this.i18nService.t(
|
||||||
|
"resellerOpenInvoiceWarningMgs",
|
||||||
|
organization.providerName,
|
||||||
|
format(warning.issued!.issuedDate),
|
||||||
|
format(warning.issued!.dueDate),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "past_due": {
|
||||||
|
return {
|
||||||
|
type: "warning",
|
||||||
|
message: this.i18nService.t(
|
||||||
|
"resellerPastDueWarningMsg",
|
||||||
|
organization.providerName,
|
||||||
|
format(warning.pastDue!.suspensionDate),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
filter((result): result is NonNullable<typeof result> => result !== null),
|
||||||
|
);
|
||||||
|
|
||||||
|
showInactiveSubscriptionDialog$ = (organization: Organization): Observable<void> =>
|
||||||
|
this.getWarning$(organization, (response) => response.inactiveSubscription).pipe(
|
||||||
|
switchMap(async (warning) => {
|
||||||
|
switch (warning.resolution) {
|
||||||
|
case "contact_provider": {
|
||||||
|
await this.dialogService.openSimpleDialog({
|
||||||
|
title: this.i18nService.t("suspendedOrganizationTitle", organization.name),
|
||||||
|
content: {
|
||||||
|
key: "suspendedManagedOrgMessage",
|
||||||
|
placeholders: [organization.providerName],
|
||||||
|
},
|
||||||
|
type: "danger",
|
||||||
|
acceptButtonText: this.i18nService.t("close"),
|
||||||
|
cancelButtonText: null,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "add_payment_method": {
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: this.i18nService.t("suspendedOrganizationTitle", organization.name),
|
||||||
|
content: { key: "suspendedOwnerOrgMessage" },
|
||||||
|
type: "danger",
|
||||||
|
acceptButtonText: this.i18nService.t("continue"),
|
||||||
|
cancelButtonText: this.i18nService.t("close"),
|
||||||
|
});
|
||||||
|
if (confirmed) {
|
||||||
|
await this.router.navigate(
|
||||||
|
["organizations", `${organization.id}`, "billing", "payment-method"],
|
||||||
|
{
|
||||||
|
state: { launchPaymentModalAutomatically: true },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "resubscribe": {
|
||||||
|
const subscription = await this.organizationApiService.getSubscription(organization.id);
|
||||||
|
const dialogReference = openChangePlanDialog(this.dialogService, {
|
||||||
|
data: {
|
||||||
|
organizationId: organization.id,
|
||||||
|
subscription: subscription,
|
||||||
|
productTierType: organization.productTierType,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await lastValueFrom(dialogReference.closed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "contact_owner": {
|
||||||
|
await this.dialogService.openSimpleDialog({
|
||||||
|
title: this.i18nService.t("suspendedOrganizationTitle", organization.name),
|
||||||
|
content: { key: "suspendedUserOrgMessage" },
|
||||||
|
type: "danger",
|
||||||
|
acceptButtonText: this.i18nService.t("close"),
|
||||||
|
cancelButtonText: null,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
private getResponse$ = (organization: Organization): Observable<OrganizationWarningsResponse> => {
|
||||||
|
const existing = this.cache$.get(organization.id as OrganizationId);
|
||||||
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
const response$ = from(this.organizationBillingApiService.getWarnings(organization.id)).pipe(
|
||||||
|
shareReplay({ bufferSize: 1, refCount: false }),
|
||||||
|
);
|
||||||
|
this.cache$.set(organization.id as OrganizationId, response$);
|
||||||
|
return response$;
|
||||||
|
};
|
||||||
|
|
||||||
|
private getWarning$ = <T>(
|
||||||
|
organization: Organization,
|
||||||
|
extract: (response: OrganizationWarningsResponse) => T | null | undefined,
|
||||||
|
): Observable<T> =>
|
||||||
|
this.getResponse$(organization).pipe(
|
||||||
|
map(extract),
|
||||||
|
takeWhile((warning): warning is T => !!warning),
|
||||||
|
take(1),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { AsyncPipe } from "@angular/common";
|
||||||
|
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
import { AnchorLinkDirective, BannerComponent } from "@bitwarden/components";
|
||||||
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
|
|
||||||
|
import {
|
||||||
|
FreeTrialWarning,
|
||||||
|
OrganizationWarningsService,
|
||||||
|
} from "../services/organization-warnings.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-free-trial-warning",
|
||||||
|
template: `
|
||||||
|
@let warning = freeTrialWarning$ | async;
|
||||||
|
|
||||||
|
@if (warning) {
|
||||||
|
<bit-banner
|
||||||
|
id="free-trial-banner"
|
||||||
|
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||||
|
icon="bwi-billing"
|
||||||
|
bannerType="premium"
|
||||||
|
[showClose]="false"
|
||||||
|
>
|
||||||
|
{{ warning.message }}
|
||||||
|
<a
|
||||||
|
bitLink
|
||||||
|
linkType="secondary"
|
||||||
|
(click)="clicked.emit()"
|
||||||
|
class="tw-cursor-pointer"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
{{ "clickHereToAddPaymentMethod" | i18n }}
|
||||||
|
</a>
|
||||||
|
</bit-banner>
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
standalone: true,
|
||||||
|
imports: [AnchorLinkDirective, AsyncPipe, BannerComponent, I18nPipe],
|
||||||
|
})
|
||||||
|
export class FreeTrialWarningComponent implements OnInit {
|
||||||
|
@Input({ required: true }) organization!: Organization;
|
||||||
|
@Output() clicked = new EventEmitter<void>();
|
||||||
|
|
||||||
|
freeTrialWarning$!: Observable<FreeTrialWarning>;
|
||||||
|
|
||||||
|
constructor(private organizationWarningsService: OrganizationWarningsService) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.freeTrialWarning$ = this.organizationWarningsService.getFreeTrialWarning$(
|
||||||
|
this.organization,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { AsyncPipe } from "@angular/common";
|
||||||
|
import { Component, Input, OnInit } from "@angular/core";
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
import { BannerComponent } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import {
|
||||||
|
OrganizationWarningsService,
|
||||||
|
ResellerRenewalWarning,
|
||||||
|
} from "../services/organization-warnings.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-reseller-renewal-warning",
|
||||||
|
template: `
|
||||||
|
@let warning = resellerRenewalWarning$ | async;
|
||||||
|
|
||||||
|
@if (warning) {
|
||||||
|
<bit-banner
|
||||||
|
id="reseller-warning-banner"
|
||||||
|
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||||
|
icon="bwi-billing"
|
||||||
|
bannerType="info"
|
||||||
|
[showClose]="false"
|
||||||
|
>
|
||||||
|
{{ warning.message }}
|
||||||
|
</bit-banner>
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
standalone: true,
|
||||||
|
imports: [AsyncPipe, BannerComponent],
|
||||||
|
})
|
||||||
|
export class ResellerRenewalWarningComponent implements OnInit {
|
||||||
|
@Input({ required: true }) organization!: Organization;
|
||||||
|
|
||||||
|
resellerRenewalWarning$!: Observable<ResellerRenewalWarning>;
|
||||||
|
|
||||||
|
constructor(private organizationWarningsService: OrganizationWarningsService) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.resellerRenewalWarning$ = this.organizationWarningsService.getResellerRenewalWarning$(
|
||||||
|
this.organization,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.response";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BillingInvoiceResponse,
|
BillingInvoiceResponse,
|
||||||
BillingTransactionResponse,
|
BillingTransactionResponse,
|
||||||
@@ -15,6 +17,8 @@ export abstract class OrganizationBillingApiServiceAbstraction {
|
|||||||
startAfter?: string,
|
startAfter?: string,
|
||||||
) => Promise<BillingTransactionResponse[]>;
|
) => Promise<BillingTransactionResponse[]>;
|
||||||
|
|
||||||
|
abstract getWarnings: (id: string) => Promise<OrganizationWarningsResponse>;
|
||||||
|
|
||||||
abstract setupBusinessUnit: (
|
abstract setupBusinessUnit: (
|
||||||
id: string,
|
id: string,
|
||||||
request: {
|
request: {
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { BaseResponse } from "../../../models/response/base.response";
|
||||||
|
|
||||||
|
export class OrganizationWarningsResponse extends BaseResponse {
|
||||||
|
freeTrial?: FreeTrialWarningResponse;
|
||||||
|
inactiveSubscription?: InactiveSubscriptionWarningResponse;
|
||||||
|
resellerRenewal?: ResellerRenewalWarningResponse;
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
const freeTrialWarning = this.getResponseProperty("FreeTrial");
|
||||||
|
if (freeTrialWarning) {
|
||||||
|
this.freeTrial = new FreeTrialWarningResponse(freeTrialWarning);
|
||||||
|
}
|
||||||
|
const inactiveSubscriptionWarning = this.getResponseProperty("InactiveSubscription");
|
||||||
|
if (inactiveSubscriptionWarning) {
|
||||||
|
this.inactiveSubscription = new InactiveSubscriptionWarningResponse(
|
||||||
|
inactiveSubscriptionWarning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const resellerWarning = this.getResponseProperty("ResellerRenewal");
|
||||||
|
if (resellerWarning) {
|
||||||
|
this.resellerRenewal = new ResellerRenewalWarningResponse(resellerWarning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FreeTrialWarningResponse extends BaseResponse {
|
||||||
|
remainingTrialDays: number;
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
this.remainingTrialDays = this.getResponseProperty("RemainingTrialDays");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InactiveSubscriptionWarningResponse extends BaseResponse {
|
||||||
|
resolution: string;
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
this.resolution = this.getResponseProperty("Resolution");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResellerRenewalWarningResponse extends BaseResponse {
|
||||||
|
type: "upcoming" | "issued" | "past_due";
|
||||||
|
upcoming?: UpcomingRenewal;
|
||||||
|
issued?: IssuedRenewal;
|
||||||
|
pastDue?: PastDueRenewal;
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
this.type = this.getResponseProperty("Type");
|
||||||
|
switch (this.type) {
|
||||||
|
case "upcoming": {
|
||||||
|
this.upcoming = new UpcomingRenewal(this.getResponseProperty("Upcoming"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "issued": {
|
||||||
|
this.issued = new IssuedRenewal(this.getResponseProperty("Issued"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "past_due": {
|
||||||
|
this.pastDue = new PastDueRenewal(this.getResponseProperty("PastDue"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UpcomingRenewal extends BaseResponse {
|
||||||
|
renewalDate: Date;
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
this.renewalDate = new Date(this.getResponseProperty("RenewalDate"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IssuedRenewal extends BaseResponse {
|
||||||
|
issuedDate: Date;
|
||||||
|
dueDate: Date;
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
this.issuedDate = new Date(this.getResponseProperty("IssuedDate"));
|
||||||
|
this.dueDate = new Date(this.getResponseProperty("DueDate"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PastDueRenewal extends BaseResponse {
|
||||||
|
suspensionDate: Date;
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
this.suspensionDate = new Date(this.getResponseProperty("SuspensionDate"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.response";
|
||||||
|
|
||||||
import { ApiService } from "../../../abstractions/api.service";
|
import { ApiService } from "../../../abstractions/api.service";
|
||||||
import { OrganizationBillingApiServiceAbstraction } from "../../abstractions/organizations/organization-billing-api.service.abstraction";
|
import { OrganizationBillingApiServiceAbstraction } from "../../abstractions/organizations/organization-billing-api.service.abstraction";
|
||||||
import {
|
import {
|
||||||
@@ -50,6 +52,18 @@ export class OrganizationBillingApiService implements OrganizationBillingApiServ
|
|||||||
return r?.map((i: any) => new BillingTransactionResponse(i)) || [];
|
return r?.map((i: any) => new BillingTransactionResponse(i)) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getWarnings(id: string): Promise<OrganizationWarningsResponse> {
|
||||||
|
const response = await this.apiService.send(
|
||||||
|
"GET",
|
||||||
|
`/organizations/${id}/billing/warnings`,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new OrganizationWarningsResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
async setupBusinessUnit(
|
async setupBusinessUnit(
|
||||||
id: string,
|
id: string,
|
||||||
request: {
|
request: {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export enum FeatureFlag {
|
|||||||
PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method",
|
PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method",
|
||||||
PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships",
|
PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships",
|
||||||
PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup",
|
PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup",
|
||||||
|
UseOrganizationWarningsService = "use-organization-warnings-service",
|
||||||
|
|
||||||
/* Data Insights and Reporting */
|
/* Data Insights and Reporting */
|
||||||
CriticalApps = "pm-14466-risk-insights-critical-application",
|
CriticalApps = "pm-14466-risk-insights-critical-application",
|
||||||
@@ -123,6 +124,7 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.PM18794_ProviderPaymentMethod]: FALSE,
|
[FeatureFlag.PM18794_ProviderPaymentMethod]: FALSE,
|
||||||
[FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE,
|
[FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE,
|
||||||
[FeatureFlag.PM19956_RequireProviderPaymentMethodDuringSetup]: FALSE,
|
[FeatureFlag.PM19956_RequireProviderPaymentMethodDuringSetup]: FALSE,
|
||||||
|
[FeatureFlag.UseOrganizationWarningsService]: FALSE,
|
||||||
|
|
||||||
/* Key Management */
|
/* Key Management */
|
||||||
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
|
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
|
||||||
|
|||||||
Reference in New Issue
Block a user