From b396b6bafbb184cfb5bad313142e64c87ceabd7e Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 8 Oct 2025 13:14:29 -0500 Subject: [PATCH] [PM-26628] Refresh inactive subscription warning after restarting subscription (#16791) * Refresh inactive subscription warning after restarting subscription * Fix tests --- .../change-plan-dialog.component.ts | 3 + .../organization-warnings.service.spec.ts | 128 ++++++++---------- .../services/organization-warnings.service.ts | 17 ++- 3 files changed, 75 insertions(+), 73 deletions(-) diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts index 2b5c27e0f09..9d093ec4514 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts @@ -50,6 +50,7 @@ import { SubscriberBillingClient, TaxClient, } from "@bitwarden/web-vault/app/billing/clients"; +import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; import { EnterBillingAddressComponent, EnterPaymentMethodComponent, @@ -221,6 +222,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { private billingNotificationService: BillingNotificationService, private subscriberBillingClient: SubscriberBillingClient, private taxClient: TaxClient, + private organizationWarningsService: OrganizationWarningsService, ) {} async ngOnInit(): Promise { @@ -808,6 +810,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { paymentMethod, billingAddress, ); + this.organizationWarningsService.refreshInactiveSubscriptionWarning(); } private async updateOrganization() { diff --git a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts index 53f72558089..8c2a7634264 100644 --- a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts +++ b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts @@ -421,11 +421,9 @@ describe("OrganizationWarningsService", () => { it("should not show dialog when no inactive subscription warning exists", (done) => { organizationBillingClient.getWarnings.mockResolvedValue({} as OrganizationWarningsResponse); - service.showInactiveSubscriptionDialog$(organization).subscribe({ - complete: () => { - expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); - done(); - }, + service.showInactiveSubscriptionDialog$(organization).subscribe(() => { + expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); + done(); }); }); @@ -437,20 +435,18 @@ describe("OrganizationWarningsService", () => { dialogService.openSimpleDialog.mockResolvedValue(true); - service.showInactiveSubscriptionDialog$(organization).subscribe({ - complete: () => { - expect(dialogService.openSimpleDialog).toHaveBeenCalledWith({ - title: "Test Organization subscription suspended", - content: { - key: "suspendedManagedOrgMessage", - placeholders: ["Test Reseller Inc"], - }, - type: "danger", - acceptButtonText: "Close", - cancelButtonText: null, - }); - done(); - }, + service.showInactiveSubscriptionDialog$(organization).subscribe(() => { + expect(dialogService.openSimpleDialog).toHaveBeenCalledWith({ + title: "Test Organization subscription suspended", + content: { + key: "suspendedManagedOrgMessage", + placeholders: ["Test Reseller Inc"], + }, + type: "danger", + acceptButtonText: "Close", + cancelButtonText: null, + }); + done(); }); }); @@ -463,21 +459,19 @@ describe("OrganizationWarningsService", () => { dialogService.openSimpleDialog.mockResolvedValue(true); router.navigate.mockResolvedValue(true); - service.showInactiveSubscriptionDialog$(organization).subscribe({ - complete: () => { - expect(dialogService.openSimpleDialog).toHaveBeenCalledWith({ - title: "Test Organization subscription suspended", - content: { key: "suspendedOwnerOrgMessage" }, - type: "danger", - acceptButtonText: "Continue", - cancelButtonText: "Close", - }); - expect(router.navigate).toHaveBeenCalledWith( - ["organizations", "org-id-123", "billing", "payment-details"], - { state: { launchPaymentModalAutomatically: true } }, - ); - done(); - }, + service.showInactiveSubscriptionDialog$(organization).subscribe(() => { + expect(dialogService.openSimpleDialog).toHaveBeenCalledWith({ + title: "Test Organization subscription suspended", + content: { key: "suspendedOwnerOrgMessage" }, + type: "danger", + acceptButtonText: "Continue", + cancelButtonText: "Close", + }); + expect(router.navigate).toHaveBeenCalledWith( + ["organizations", "org-id-123", "billing", "payment-details"], + { state: { launchPaymentModalAutomatically: true } }, + ); + done(); }); }); @@ -490,14 +484,12 @@ describe("OrganizationWarningsService", () => { dialogService.openSimpleDialog.mockResolvedValue(true); router.navigate.mockResolvedValue(true); - service.showInactiveSubscriptionDialog$(organization).subscribe({ - complete: () => { - expect(router.navigate).toHaveBeenCalledWith( - ["organizations", "org-id-123", "billing", "payment-details"], - { state: { launchPaymentModalAutomatically: true } }, - ); - done(); - }, + service.showInactiveSubscriptionDialog$(organization).subscribe(() => { + expect(router.navigate).toHaveBeenCalledWith( + ["organizations", "org-id-123", "billing", "payment-details"], + { state: { launchPaymentModalAutomatically: true } }, + ); + done(); }); }); @@ -509,12 +501,10 @@ describe("OrganizationWarningsService", () => { dialogService.openSimpleDialog.mockResolvedValue(false); - service.showInactiveSubscriptionDialog$(organization).subscribe({ - complete: () => { - expect(dialogService.openSimpleDialog).toHaveBeenCalled(); - expect(router.navigate).not.toHaveBeenCalled(); - done(); - }, + service.showInactiveSubscriptionDialog$(organization).subscribe(() => { + expect(dialogService.openSimpleDialog).toHaveBeenCalled(); + expect(router.navigate).not.toHaveBeenCalled(); + done(); }); }); @@ -534,18 +524,16 @@ describe("OrganizationWarningsService", () => { (openChangePlanDialog as jest.Mock).mockReturnValue(mockDialogRef); - service.showInactiveSubscriptionDialog$(organization).subscribe({ - complete: () => { - expect(organizationApiService.getSubscription).toHaveBeenCalledWith(organization.id); - expect(openChangePlanDialog).toHaveBeenCalledWith(dialogService, { - data: { - organizationId: organization.id, - subscription: subscription, - productTierType: organization.productTierType, - }, - }); - done(); - }, + service.showInactiveSubscriptionDialog$(organization).subscribe(() => { + expect(organizationApiService.getSubscription).toHaveBeenCalledWith(organization.id); + expect(openChangePlanDialog).toHaveBeenCalledWith(dialogService, { + data: { + organizationId: organization.id, + subscription: subscription, + productTierType: organization.productTierType, + }, + }); + done(); }); }); @@ -557,17 +545,15 @@ describe("OrganizationWarningsService", () => { dialogService.openSimpleDialog.mockResolvedValue(true); - service.showInactiveSubscriptionDialog$(organization).subscribe({ - complete: () => { - expect(dialogService.openSimpleDialog).toHaveBeenCalledWith({ - title: "Test Organization subscription suspended", - content: { key: "suspendedUserOrgMessage" }, - type: "danger", - acceptButtonText: "Close", - cancelButtonText: null, - }); - done(); - }, + service.showInactiveSubscriptionDialog$(organization).subscribe(() => { + expect(dialogService.openSimpleDialog).toHaveBeenCalledWith({ + title: "Test Organization subscription suspended", + content: { key: "suspendedUserOrgMessage" }, + type: "danger", + acceptButtonText: "Close", + cancelButtonText: null, + }); + done(); }); }); }); diff --git a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts index 46a34def28b..8bec7acffe1 100644 --- a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts +++ b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts @@ -46,6 +46,7 @@ export class OrganizationWarningsService { private refreshFreeTrialWarningTrigger = new Subject(); private refreshTaxIdWarningTrigger = new Subject(); + private refreshInactiveSubscriptionWarningTrigger = new Subject(); private taxIdWarningRefreshedSubject = new BehaviorSubject(null); taxIdWarningRefreshed$ = this.taxIdWarningRefreshedSubject.asObservable(); @@ -164,12 +165,24 @@ export class OrganizationWarningsService { refreshFreeTrialWarning = () => this.refreshFreeTrialWarningTrigger.next(); + refreshInactiveSubscriptionWarning = () => this.refreshInactiveSubscriptionWarningTrigger.next(); + refreshTaxIdWarning = () => this.refreshTaxIdWarningTrigger.next(); showInactiveSubscriptionDialog$ = (organization: Organization): Observable => - this.getWarning$(organization, (response) => response.inactiveSubscription).pipe( - filter((warning) => warning !== null), + merge( + this.getWarning$(organization, (response) => response.inactiveSubscription), + this.refreshInactiveSubscriptionWarningTrigger.pipe( + switchMap(() => + this.getWarning$(organization, (response) => response.inactiveSubscription, true), + ), + ), + ).pipe( switchMap(async (warning) => { + if (!warning) { + return; + } + switch (warning.resolution) { case "contact_provider": { await this.dialogService.openSimpleDialog({