From 6e7ca0683a295ed1aebe05a8f87bee7e8d72803c Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Wed, 4 Feb 2026 14:53:55 -0500 Subject: [PATCH] refactor(billing): Remove premium to org upgrade logic from UnifiedUpgradeDialog --- .../unified-upgrade-dialog.component.html | 47 ++-- .../unified-upgrade-dialog.component.spec.ts | 250 ++---------------- .../unified-upgrade-dialog.component.ts | 88 +----- 3 files changed, 41 insertions(+), 344 deletions(-) diff --git a/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.html b/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.html index ab14fbbee6c..2cac1c202b5 100644 --- a/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.html +++ b/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.html @@ -1,33 +1,16 @@ - -@if (showPremiumOrgFlow()) { - @if (step() == PlanSelectionStep) { - - } @else if (step() == PaymentStep && selectedPlan() !== null && account() !== null) { - - } -} @else { - - @if (step() == PlanSelectionStep) { - - } @else if (step() == PaymentStep && selectedPersonalPlanId() !== null && account() !== null) { - - } + +@if (step() == PlanSelectionStep) { + +} @else if (step() == PaymentStep && selectedPlan() !== null && account() !== null) { + } diff --git a/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.spec.ts b/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.spec.ts index ab9e962bd0a..b3b8dbb170d 100644 --- a/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.spec.ts +++ b/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.spec.ts @@ -3,27 +3,17 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { Router } from "@angular/router"; import { mock } from "jest-mock-extended"; -import { of } from "rxjs"; import { PremiumInterestStateService } from "@bitwarden/angular/billing/services/premium-interest/premium-interest-state.service.abstraction"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { PersonalSubscriptionPricingTierId, PersonalSubscriptionPricingTierIds, - BusinessSubscriptionPricingTierId, } from "@bitwarden/common/billing/types/subscription-pricing-tier"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { mockAccountInfoWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; import { DIALOG_DATA, DialogRef } from "@bitwarden/components"; -import { PremiumOrgUpgradeComponent } from "../premium-org-upgrade/premium-org-upgrade.component"; -import { - PremiumOrgUpgradePaymentComponent, - PremiumOrgUpgradePaymentResult, - PremiumOrgUpgradePaymentStatus, -} from "../premium-org-upgrade-payment/premium-org-upgrade-payment.component"; import { UpgradeAccountComponent, UpgradeAccountStatus, @@ -67,44 +57,12 @@ class MockUpgradePaymentComponent { complete = output(); } -@Component({ - selector: "app-premium-org-upgrade", - template: "", - standalone: true, - providers: [PremiumOrgUpgradeComponent], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -class MockPremiumOrgUpgradeComponent { - readonly dialogTitleMessageOverride = input(null); - readonly hideContinueWithoutUpgradingButton = input(false); - planSelected = output(); - closeClicked = output(); -} - -@Component({ - selector: "app-premium-org-upgrade-payment", - template: "", - standalone: true, - providers: [PremiumOrgUpgradePaymentComponent], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -class MockPremiumOrgUpgradePaymentComponent { - readonly selectedPlanId = input< - PersonalSubscriptionPricingTierId | BusinessSubscriptionPricingTierId | null - >(null); - readonly account = input(null); - goBack = output(); - complete = output<{ status: PremiumOrgUpgradePaymentStatus; organizationId: string | null }>(); -} - describe("UnifiedUpgradeDialogComponent", () => { let component: UnifiedUpgradeDialogComponent; let fixture: ComponentFixture; const mockDialogRef = mock(); const mockRouter = mock(); const mockPremiumInterestStateService = mock(); - const mockBillingAccountProfileStateService = mock(); - const mockConfigService = mock(); const mockAccount: Account = { id: "user-id" as UserId, ...mockAccountInfoWith({ @@ -140,29 +98,14 @@ describe("UnifiedUpgradeDialogComponent", () => { { provide: DIALOG_DATA, useValue: dialogData }, { provide: Router, useValue: mockRouter }, { provide: PremiumInterestStateService, useValue: mockPremiumInterestStateService }, - { - provide: BillingAccountProfileStateService, - useValue: mockBillingAccountProfileStateService, - }, - { provide: ConfigService, useValue: mockConfigService }, ], }) .overrideComponent(UnifiedUpgradeDialogComponent, { remove: { - imports: [ - UpgradeAccountComponent, - UpgradePaymentComponent, - PremiumOrgUpgradeComponent, - PremiumOrgUpgradePaymentComponent, - ], + imports: [UpgradeAccountComponent, UpgradePaymentComponent], }, add: { - imports: [ - MockUpgradeAccountComponent, - MockUpgradePaymentComponent, - MockPremiumOrgUpgradeComponent, - MockPremiumOrgUpgradePaymentComponent, - ], + imports: [MockUpgradeAccountComponent, MockUpgradePaymentComponent], }, }) .compileComponents(); @@ -184,8 +127,6 @@ describe("UnifiedUpgradeDialogComponent", () => { // Default mock: no premium interest mockPremiumInterestStateService.getPremiumInterest.mockResolvedValue(false); - mockBillingAccountProfileStateService.hasPremiumPersonally$.mockReturnValue(of(true)); - mockConfigService.getFeatureFlag$.mockReturnValue(of(false)); await TestBed.configureTestingModule({ imports: [UnifiedUpgradeDialogComponent], providers: [ @@ -193,29 +134,14 @@ describe("UnifiedUpgradeDialogComponent", () => { { provide: DIALOG_DATA, useValue: defaultDialogData }, { provide: Router, useValue: mockRouter }, { provide: PremiumInterestStateService, useValue: mockPremiumInterestStateService }, - { - provide: BillingAccountProfileStateService, - useValue: mockBillingAccountProfileStateService, - }, - { provide: ConfigService, useValue: mockConfigService }, ], }) .overrideComponent(UnifiedUpgradeDialogComponent, { remove: { - imports: [ - UpgradeAccountComponent, - UpgradePaymentComponent, - PremiumOrgUpgradeComponent, - PremiumOrgUpgradePaymentComponent, - ], + imports: [UpgradeAccountComponent, UpgradePaymentComponent], }, add: { - imports: [ - MockUpgradeAccountComponent, - MockUpgradePaymentComponent, - MockPremiumOrgUpgradeComponent, - MockPremiumOrgUpgradePaymentComponent, - ], + imports: [MockUpgradeAccountComponent, MockUpgradePaymentComponent], }, }) .compileComponents(); @@ -477,170 +403,30 @@ describe("UnifiedUpgradeDialogComponent", () => { }); describe("Child Component Display Logic", () => { - describe("Plan Selection Step", () => { - it("should display app-upgrade-account when user does not have premium personally", async () => { - mockBillingAccountProfileStateService.hasPremiumPersonally$.mockReturnValue(of(false)); - mockConfigService.getFeatureFlag$.mockReturnValue(of(false)); + it("should display app-upgrade-account on plan selection step", async () => { + const { fixture } = await createComponentWithDialogData(defaultDialogData); - const { fixture } = await createComponentWithDialogData(defaultDialogData); + const upgradeAccountElement = fixture.nativeElement.querySelector("app-upgrade-account"); - const upgradeAccountElement = fixture.nativeElement.querySelector("app-upgrade-account"); - const premiumOrgUpgradeElement = - fixture.nativeElement.querySelector("app-premium-org-upgrade"); - - expect(upgradeAccountElement).toBeTruthy(); - expect(premiumOrgUpgradeElement).toBeFalsy(); - }); - - it("should display app-upgrade-account when user has premium but feature flag is disabled", async () => { - mockBillingAccountProfileStateService.hasPremiumPersonally$.mockReturnValue(of(true)); - mockConfigService.getFeatureFlag$.mockReturnValue(of(false)); - - const { fixture } = await createComponentWithDialogData(defaultDialogData); - - const upgradeAccountElement = fixture.nativeElement.querySelector("app-upgrade-account"); - const premiumOrgUpgradeElement = - fixture.nativeElement.querySelector("app-premium-org-upgrade"); - - expect(upgradeAccountElement).toBeTruthy(); - expect(premiumOrgUpgradeElement).toBeFalsy(); - }); - - it("should display app-premium-org-upgrade when user has premium and feature flag is enabled", async () => { - mockBillingAccountProfileStateService.hasPremiumPersonally$.mockReturnValue(of(true)); - mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); - - const { fixture } = await createComponentWithDialogData(defaultDialogData); - - const upgradeAccountElement = fixture.nativeElement.querySelector("app-upgrade-account"); - const premiumOrgUpgradeElement = - fixture.nativeElement.querySelector("app-premium-org-upgrade"); - - expect(upgradeAccountElement).toBeFalsy(); - expect(premiumOrgUpgradeElement).toBeTruthy(); - }); + expect(upgradeAccountElement).toBeTruthy(); }); - describe("Payment Step", () => { - it("should display app-upgrade-payment when user does not have premium personally", async () => { - mockBillingAccountProfileStateService.hasPremiumPersonally$.mockReturnValue(of(false)); + it("should display app-upgrade-payment on payment step", async () => { + const customDialogData: UnifiedUpgradeDialogParams = { + account: mockAccount, + initialStep: UnifiedUpgradeDialogStep.Payment, + selectedPlan: PersonalSubscriptionPricingTierIds.Premium, + }; - const customDialogData: UnifiedUpgradeDialogParams = { - account: mockAccount, - initialStep: UnifiedUpgradeDialogStep.Payment, - selectedPlan: PersonalSubscriptionPricingTierIds.Premium, - }; + const { fixture } = await createComponentWithDialogData(customDialogData); - const { fixture } = await createComponentWithDialogData(customDialogData); + const upgradePaymentElement = fixture.nativeElement.querySelector("app-upgrade-payment"); - const upgradePaymentElement = fixture.nativeElement.querySelector("app-upgrade-payment"); - const premiumOrgUpgradePaymentElement = fixture.nativeElement.querySelector( - "app-premium-org-upgrade-payment", - ); - - expect(upgradePaymentElement).toBeTruthy(); - expect(premiumOrgUpgradePaymentElement).toBeFalsy(); - }); - - it("should display app-premium-org-upgrade-payment when user has premium personally", async () => { - mockBillingAccountProfileStateService.hasPremiumPersonally$.mockReturnValue(of(true)); - mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); - - const customDialogData: UnifiedUpgradeDialogParams = { - account: mockAccount, - initialStep: UnifiedUpgradeDialogStep.Payment, - selectedPlan: "teams" as BusinessSubscriptionPricingTierId, - }; - - const { fixture } = await createComponentWithDialogData(customDialogData); - - const upgradePaymentElement = fixture.nativeElement.querySelector("app-upgrade-payment"); - const premiumOrgUpgradePaymentElement = fixture.nativeElement.querySelector( - "app-premium-org-upgrade-payment", - ); - - expect(upgradePaymentElement).toBeFalsy(); - expect(premiumOrgUpgradePaymentElement).toBeTruthy(); - }); + expect(upgradePaymentElement).toBeTruthy(); }); }); - describe("Premium Org Upgrade", () => { - it("should handle selecting a business plan (Teams) and move to payment step", async () => { - const { component } = await createComponentWithDialogData(defaultDialogData); - - component["onPlanSelected"]("teams" as BusinessSubscriptionPricingTierId); - - expect(component["selectedPlan"]()).toBe("teams"); - expect(component["step"]()).toBe(UnifiedUpgradeDialogStep.Payment); - }); - - it("should handle completing premium org upgrade to Teams successfully", async () => { - const { component } = await createComponentWithDialogData(defaultDialogData); - mockRouter.navigate.mockResolvedValue(true); - - const result = { - status: "upgradedToTeams" as const, - organizationId: "org-123", - }; - - await component["onComplete"](result); - - expect(mockDialogRef.close).toHaveBeenCalledWith({ - status: "upgradedToTeams", - organizationId: "org-123", - }); - }); - - it("should handle completing premium org upgrade to Enterprise successfully", async () => { - const { component } = await createComponentWithDialogData(defaultDialogData); - mockRouter.navigate.mockResolvedValue(true); - - const result = { - status: "upgradedToEnterprise" as const, - organizationId: "org-456", - }; - - await component["onComplete"](result); - - expect(mockDialogRef.close).toHaveBeenCalledWith({ - status: "upgradedToEnterprise", - organizationId: "org-456", - }); - }); - - it("should handle user closing during premium org plan selection", async () => { - const { component } = await createComponentWithDialogData(defaultDialogData); - - await component["onCloseClicked"](); - - expect(mockDialogRef.close).toHaveBeenCalledWith({ status: "closed" }); - }); - - it("should go back from premium org payment step to plan selection", async () => { - const { component } = await createComponentWithDialogData(defaultDialogData); - component["step"].set(UnifiedUpgradeDialogStep.Payment); - component["selectedPlan"].set("teams" as BusinessSubscriptionPricingTierId); - - await component["previousStep"](); - - expect(component["step"]()).toBe(UnifiedUpgradeDialogStep.PlanSelection); - expect(component["selectedPlan"]()).toBeNull(); - }); - - it("should handle closed status during premium org upgrade", async () => { - const { component } = await createComponentWithDialogData(defaultDialogData); - - const result: PremiumOrgUpgradePaymentResult = { status: "closed", organizationId: null }; - - await component["onComplete"](result); - - expect(mockDialogRef.close).toHaveBeenCalledWith({ - status: "closed", - organizationId: null, - }); - }); - + describe("redirectOnCompletion", () => { it("should handle redirectOnCompletion for families upgrade with organization", async () => { const customDialogData: UnifiedUpgradeDialogParams = { account: mockAccount, diff --git a/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.ts b/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.ts index 887c9205230..63017760195 100644 --- a/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.ts +++ b/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.ts @@ -1,26 +1,11 @@ import { DIALOG_DATA } from "@angular/cdk/dialog"; import { CommonModule } from "@angular/common"; -import { - ChangeDetectionStrategy, - Component, - computed, - Inject, - OnInit, - signal, -} from "@angular/core"; -import { toSignal } from "@angular/core/rxjs-interop"; +import { ChangeDetectionStrategy, Component, Inject, OnInit, signal } from "@angular/core"; import { Router } from "@angular/router"; import { PremiumInterestStateService } from "@bitwarden/angular/billing/services/premium-interest/premium-interest-state.service.abstraction"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; -import { - BusinessSubscriptionPricingTierId, - PersonalSubscriptionPricingTierId, - PersonalSubscriptionPricingTierIds, -} from "@bitwarden/common/billing/types/subscription-pricing-tier"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { PersonalSubscriptionPricingTierId } from "@bitwarden/common/billing/types/subscription-pricing-tier"; import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; import { ButtonModule, @@ -32,11 +17,6 @@ import { import { AccountBillingClient, PreviewInvoiceClient } from "../../../clients"; import { BillingServicesModule } from "../../../services"; -import { PremiumOrgUpgradeComponent } from "../premium-org-upgrade/premium-org-upgrade.component"; -import { - PremiumOrgUpgradePaymentComponent, - PremiumOrgUpgradePaymentResult, -} from "../premium-org-upgrade-payment/premium-org-upgrade-payment.component"; import { UpgradeAccountComponent } from "../upgrade-account/upgrade-account.component"; import { UpgradePaymentService } from "../upgrade-payment/services/upgrade-payment.service"; import { @@ -48,8 +28,6 @@ export const UnifiedUpgradeDialogStatus = { Closed: "closed", UpgradedToPremium: "upgradedToPremium", UpgradedToFamilies: "upgradedToFamilies", - UpgradedToTeams: "upgradedToTeams", - UpgradedToEnterprise: "upgradedToEnterprise", } as const; export const UnifiedUpgradeDialogStep = { @@ -79,7 +57,7 @@ export type UnifiedUpgradeDialogResult = { export type UnifiedUpgradeDialogParams = { account: Account; initialStep?: UnifiedUpgradeDialogStep | null; - selectedPlan?: PersonalSubscriptionPricingTierId | BusinessSubscriptionPricingTierId | null; + selectedPlan?: PersonalSubscriptionPricingTierId | null; planSelectionStepTitleOverride?: string | null; hideContinueWithoutUpgradingButton?: boolean; redirectOnCompletion?: boolean; @@ -95,8 +73,6 @@ export type UnifiedUpgradeDialogParams = { UpgradeAccountComponent, UpgradePaymentComponent, BillingServicesModule, - PremiumOrgUpgradeComponent, - PremiumOrgUpgradePaymentComponent, ], providers: [UpgradePaymentService, AccountBillingClient, PreviewInvoiceClient], templateUrl: "./unified-upgrade-dialog.component.html", @@ -106,43 +82,11 @@ export class UnifiedUpgradeDialogComponent implements OnInit { protected readonly step = signal( UnifiedUpgradeDialogStep.PlanSelection, ); - protected readonly selectedPlan = signal< - PersonalSubscriptionPricingTierId | BusinessSubscriptionPricingTierId | null - >(null); + protected readonly selectedPlan = signal(null); protected readonly account = signal(null); protected readonly planSelectionStepTitleOverride = signal(null); protected readonly hideContinueWithoutUpgradingButton = signal(false); protected readonly hasPremiumInterest = signal(false); - protected readonly hasPremiumPersonally = toSignal( - this.billingAccountProfileStateService.hasPremiumPersonally$(this.params.account.id), - { initialValue: false }, - ); - protected readonly premiumToOrganizationUpgradeEnabled = toSignal( - this.configService.getFeatureFlag$(FeatureFlag.PM29593_PremiumToOrganizationUpgrade), - { initialValue: false }, - ); - protected readonly showPremiumOrgFlow = computed( - () => this.hasPremiumPersonally() && this.premiumToOrganizationUpgradeEnabled(), - ); - /** - * Type-safe computed signal for app-upgrade-payment component. - * Returns the selected plan only when it's a personal plan (Premium or Families). - * When showPremiumOrgFlow is true, this will be null since business plans are handled separately. - */ - protected readonly selectedPersonalPlanId = computed( - () => { - const plan = this.selectedPlan(); - // When showing premium org flow, user is selecting business plans (Teams/Enterprise) - // Standard flow uses personal plans (Premium/Families) - if ( - plan === PersonalSubscriptionPricingTierIds.Premium || - plan === PersonalSubscriptionPricingTierIds.Families - ) { - return plan; - } - return null; - }, - ); protected readonly PaymentStep = UnifiedUpgradeDialogStep.Payment; protected readonly PlanSelectionStep = UnifiedUpgradeDialogStep.PlanSelection; @@ -152,8 +96,6 @@ export class UnifiedUpgradeDialogComponent implements OnInit { @Inject(DIALOG_DATA) private params: UnifiedUpgradeDialogParams, private router: Router, private premiumInterestStateService: PremiumInterestStateService, - private billingAccountProfileStateService: BillingAccountProfileStateService, - private configService: ConfigService, ) {} async ngOnInit(): Promise { @@ -179,9 +121,7 @@ export class UnifiedUpgradeDialogComponent implements OnInit { } } - protected onPlanSelected( - planId: PersonalSubscriptionPricingTierId | BusinessSubscriptionPricingTierId, - ): void { + protected onPlanSelected(planId: PersonalSubscriptionPricingTierId): void { this.selectedPlan.set(planId); this.nextStep(); } @@ -210,17 +150,9 @@ export class UnifiedUpgradeDialogComponent implements OnInit { } } - protected async onComplete( - result: UpgradePaymentResult | PremiumOrgUpgradePaymentResult, - ): Promise { + protected async onComplete(result: UpgradePaymentResult): Promise { let status: UnifiedUpgradeDialogStatus; switch (result.status) { - case "upgradedToTeams": - status = UnifiedUpgradeDialogStatus.UpgradedToTeams; - break; - case "upgradedToEnterprise": - status = UnifiedUpgradeDialogStatus.UpgradedToEnterprise; - break; case "upgradedToPremium": status = UnifiedUpgradeDialogStatus.UpgradedToPremium; break; @@ -248,14 +180,10 @@ export class UnifiedUpgradeDialogComponent implements OnInit { if ( this.params.redirectOnCompletion && (status === UnifiedUpgradeDialogStatus.UpgradedToPremium || - status === UnifiedUpgradeDialogStatus.UpgradedToFamilies || - status === UnifiedUpgradeDialogStatus.UpgradedToEnterprise || - status === UnifiedUpgradeDialogStatus.UpgradedToTeams) + status === UnifiedUpgradeDialogStatus.UpgradedToFamilies) ) { const redirectUrl = - status === UnifiedUpgradeDialogStatus.UpgradedToFamilies || - status === UnifiedUpgradeDialogStatus.UpgradedToEnterprise || - status === UnifiedUpgradeDialogStatus.UpgradedToTeams + status === UnifiedUpgradeDialogStatus.UpgradedToFamilies ? `/organizations/${result.organizationId}/vault` : "/settings/subscription/user-subscription"; await this.router.navigate([redirectUrl]);