1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-05 11:13:44 +00:00

fix(billing): unified upgrade dialog add feature flag and tests

This commit is contained in:
Stephon Brown
2026-01-26 14:41:32 -05:00
parent 3de61202de
commit de2d5d8897
3 changed files with 103 additions and 15 deletions

View File

@@ -1,5 +1,5 @@
@if (step() == PlanSelectionStep) {
@if (hasPremiumPersonally()) {
@if (hasPremiumPersonally() && premiumToOrganizationUpgradeEnabled()) {
<app-premium-org-upgrade
(planSelected)="onPlanSelected($event)"
(closeClicked)="onCloseClicked()"

View File

@@ -13,6 +13,7 @@ import {
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";
@@ -103,6 +104,7 @@ describe("UnifiedUpgradeDialogComponent", () => {
const mockRouter = mock<Router>();
const mockPremiumInterestStateService = mock<PremiumInterestStateService>();
const mockBillingAccountProfileStateService = mock<BillingAccountProfileStateService>();
const mockConfigService = mock<ConfigService>();
const mockAccount: Account = {
id: "user-id" as UserId,
...mockAccountInfoWith({
@@ -142,6 +144,7 @@ describe("UnifiedUpgradeDialogComponent", () => {
provide: BillingAccountProfileStateService,
useValue: mockBillingAccountProfileStateService,
},
{ provide: ConfigService, useValue: mockConfigService },
],
})
.overrideComponent(UnifiedUpgradeDialogComponent, {
@@ -182,6 +185,7 @@ 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,6 +197,7 @@ describe("UnifiedUpgradeDialogComponent", () => {
provide: BillingAccountProfileStateService,
useValue: mockBillingAccountProfileStateService,
},
{ provide: ConfigService, useValue: mockConfigService },
],
})
.overrideComponent(UnifiedUpgradeDialogComponent, {
@@ -471,7 +476,95 @@ describe("UnifiedUpgradeDialogComponent", () => {
});
});
describe("Premium Org Upgrade edge cases", () => {
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));
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-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();
});
});
describe("Payment Step", () => {
it("should display app-upgrade-payment when user does not have premium personally", async () => {
mockBillingAccountProfileStateService.hasPremiumPersonally$.mockReturnValue(of(false));
const customDialogData: UnifiedUpgradeDialogParams = {
account: mockAccount,
initialStep: UnifiedUpgradeDialogStep.Payment,
selectedPlan: PersonalSubscriptionPricingTierIds.Premium,
};
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).toBeTruthy();
expect(premiumOrgUpgradePaymentElement).toBeFalsy();
});
it("should display app-premium-org-upgrade-payment when user has premium personally", async () => {
mockBillingAccountProfileStateService.hasPremiumPersonally$.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();
});
});
});
describe("Premium Org Upgrade", () => {
it("should handle selecting a business plan (Teams) and move to payment step", async () => {
const { component } = await createComponentWithDialogData(defaultDialogData);
@@ -534,19 +627,6 @@ describe("UnifiedUpgradeDialogComponent", () => {
expect(component["selectedPlan"]()).toBeNull();
});
it("should initialize with business plan when specified", async () => {
const customDialogData: UnifiedUpgradeDialogParams = {
account: mockAccount,
initialStep: UnifiedUpgradeDialogStep.Payment,
selectedPlan: "teams" as BusinessSubscriptionPricingTierId,
};
const { component: customComponent } = await createComponentWithDialogData(customDialogData);
expect(customComponent["step"]()).toBe(UnifiedUpgradeDialogStep.Payment);
expect(customComponent["selectedPlan"]()).toBe("teams");
});
it("should handle closed status during premium org upgrade", async () => {
const { component } = await createComponentWithDialogData(defaultDialogData);

View File

@@ -18,6 +18,8 @@ import {
BusinessSubscriptionPricingTierId,
PersonalSubscriptionPricingTierId,
} 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 { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
import {
ButtonModule,
@@ -115,6 +117,11 @@ export class UnifiedUpgradeDialogComponent implements OnInit {
{ initialValue: false },
);
readonly premiumToOrganizationUpgradeEnabled = toSignal(
this.configService.getFeatureFlag$(FeatureFlag.PM29593_PremiumToOrganizationUpgrade),
{ initialValue: false },
);
// Type-narrowed computed signal for app-upgrade-payment
// When hasPremiumPersonally is false, selectedPlan will only contain PersonalSubscriptionPricingTierId
protected readonly selectedPersonalPlanId = computed<PersonalSubscriptionPricingTierId | null>(
@@ -130,6 +137,7 @@ export class UnifiedUpgradeDialogComponent implements OnInit {
private router: Router,
private premiumInterestStateService: PremiumInterestStateService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private configService: ConfigService,
) {}
async ngOnInit(): Promise<void> {