mirror of
https://github.com/bitwarden/browser
synced 2026-01-06 10:33:57 +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:
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user