mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-279699] Clear premium interest when user subscribes or closes dialog (#17221)
* Clear premium interest when user subscribes to premium or backs out of dialog * Kyle's feedback
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { Component, input, output } from "@angular/core";
|
||||
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 { PremiumInterestStateService } from "@bitwarden/angular/billing/services/premium-interest/premium-interest-state.service.abstraction";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
PersonalSubscriptionPricingTierId,
|
||||
@@ -58,6 +60,8 @@ describe("UnifiedUpgradeDialogComponent", () => {
|
||||
let component: UnifiedUpgradeDialogComponent;
|
||||
let fixture: ComponentFixture<UnifiedUpgradeDialogComponent>;
|
||||
const mockDialogRef = mock<DialogRef>();
|
||||
const mockRouter = mock<Router>();
|
||||
const mockPremiumInterestStateService = mock<PremiumInterestStateService>();
|
||||
|
||||
const mockAccount: Account = {
|
||||
id: "user-id" as UserId,
|
||||
@@ -74,11 +78,16 @@ describe("UnifiedUpgradeDialogComponent", () => {
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
// Reset mocks
|
||||
jest.clearAllMocks();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule, UnifiedUpgradeDialogComponent],
|
||||
providers: [
|
||||
{ provide: DialogRef, useValue: mockDialogRef },
|
||||
{ provide: DIALOG_DATA, useValue: defaultDialogData },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{ provide: PremiumInterestStateService, useValue: mockPremiumInterestStateService },
|
||||
],
|
||||
})
|
||||
.overrideComponent(UnifiedUpgradeDialogComponent, {
|
||||
@@ -121,6 +130,8 @@ describe("UnifiedUpgradeDialogComponent", () => {
|
||||
providers: [
|
||||
{ provide: DialogRef, useValue: mockDialogRef },
|
||||
{ provide: DIALOG_DATA, useValue: customDialogData },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{ provide: PremiumInterestStateService, useValue: mockPremiumInterestStateService },
|
||||
],
|
||||
})
|
||||
.overrideComponent(UnifiedUpgradeDialogComponent, {
|
||||
@@ -161,6 +172,8 @@ describe("UnifiedUpgradeDialogComponent", () => {
|
||||
providers: [
|
||||
{ provide: DialogRef, useValue: mockDialogRef },
|
||||
{ provide: DIALOG_DATA, useValue: customDialogData },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{ provide: PremiumInterestStateService, useValue: mockPremiumInterestStateService },
|
||||
],
|
||||
})
|
||||
.overrideComponent(UnifiedUpgradeDialogComponent, {
|
||||
@@ -191,11 +204,11 @@ describe("UnifiedUpgradeDialogComponent", () => {
|
||||
});
|
||||
|
||||
describe("previousStep", () => {
|
||||
it("should go back to plan selection and clear selected plan", () => {
|
||||
it("should go back to plan selection and clear selected plan", async () => {
|
||||
component["step"].set(UnifiedUpgradeDialogStep.Payment);
|
||||
component["selectedPlan"].set(PersonalSubscriptionPricingTierIds.Premium);
|
||||
|
||||
component["previousStep"]();
|
||||
await component["previousStep"]();
|
||||
|
||||
expect(component["step"]()).toBe(UnifiedUpgradeDialogStep.PlanSelection);
|
||||
expect(component["selectedPlan"]()).toBeNull();
|
||||
@@ -222,6 +235,8 @@ describe("UnifiedUpgradeDialogComponent", () => {
|
||||
providers: [
|
||||
{ provide: DialogRef, useValue: mockDialogRef },
|
||||
{ provide: DIALOG_DATA, useValue: customDialogData },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{ provide: PremiumInterestStateService, useValue: mockPremiumInterestStateService },
|
||||
],
|
||||
})
|
||||
.overrideComponent(UnifiedUpgradeDialogComponent, {
|
||||
@@ -241,4 +256,169 @@ describe("UnifiedUpgradeDialogComponent", () => {
|
||||
expect(customComponent["hideContinueWithoutUpgradingButton"]()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("onComplete with premium interest", () => {
|
||||
it("should check premium interest, clear it, and route to /vault when premium interest exists", async () => {
|
||||
mockPremiumInterestStateService.getPremiumInterest.mockResolvedValue(true);
|
||||
mockPremiumInterestStateService.clearPremiumInterest.mockResolvedValue();
|
||||
mockRouter.navigate.mockResolvedValue(true);
|
||||
|
||||
const result: UpgradePaymentResult = {
|
||||
status: "upgradedToPremium",
|
||||
organizationId: null,
|
||||
};
|
||||
|
||||
await component["onComplete"](result);
|
||||
|
||||
expect(mockPremiumInterestStateService.getPremiumInterest).toHaveBeenCalledWith(
|
||||
mockAccount.id,
|
||||
);
|
||||
expect(mockPremiumInterestStateService.clearPremiumInterest).toHaveBeenCalledWith(
|
||||
mockAccount.id,
|
||||
);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(["/vault"]);
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({
|
||||
status: "upgradedToPremium",
|
||||
organizationId: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("should not clear premium interest when upgrading to families", async () => {
|
||||
const result: UpgradePaymentResult = {
|
||||
status: "upgradedToFamilies",
|
||||
organizationId: "org-123",
|
||||
};
|
||||
|
||||
await component["onComplete"](result);
|
||||
|
||||
expect(mockPremiumInterestStateService.getPremiumInterest).not.toHaveBeenCalled();
|
||||
expect(mockPremiumInterestStateService.clearPremiumInterest).not.toHaveBeenCalled();
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({
|
||||
status: "upgradedToFamilies",
|
||||
organizationId: "org-123",
|
||||
});
|
||||
});
|
||||
|
||||
it("should use standard redirect when no premium interest exists", async () => {
|
||||
TestBed.resetTestingModule();
|
||||
|
||||
const customDialogData: UnifiedUpgradeDialogParams = {
|
||||
account: mockAccount,
|
||||
redirectOnCompletion: true,
|
||||
};
|
||||
|
||||
mockPremiumInterestStateService.getPremiumInterest.mockResolvedValue(false);
|
||||
mockRouter.navigate.mockResolvedValue(true);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule, UnifiedUpgradeDialogComponent],
|
||||
providers: [
|
||||
{ provide: DialogRef, useValue: mockDialogRef },
|
||||
{ provide: DIALOG_DATA, useValue: customDialogData },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{ provide: PremiumInterestStateService, useValue: mockPremiumInterestStateService },
|
||||
],
|
||||
})
|
||||
.overrideComponent(UnifiedUpgradeDialogComponent, {
|
||||
remove: {
|
||||
imports: [UpgradeAccountComponent, UpgradePaymentComponent],
|
||||
},
|
||||
add: {
|
||||
imports: [MockUpgradeAccountComponent, MockUpgradePaymentComponent],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
const customFixture = TestBed.createComponent(UnifiedUpgradeDialogComponent);
|
||||
const customComponent = customFixture.componentInstance;
|
||||
customFixture.detectChanges();
|
||||
|
||||
const result: UpgradePaymentResult = {
|
||||
status: "upgradedToPremium",
|
||||
organizationId: null,
|
||||
};
|
||||
|
||||
await customComponent["onComplete"](result);
|
||||
|
||||
expect(mockPremiumInterestStateService.getPremiumInterest).toHaveBeenCalledWith(
|
||||
mockAccount.id,
|
||||
);
|
||||
expect(mockPremiumInterestStateService.clearPremiumInterest).not.toHaveBeenCalled();
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith([
|
||||
"/settings/subscription/user-subscription",
|
||||
]);
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({
|
||||
status: "upgradedToPremium",
|
||||
organizationId: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("onCloseClicked with premium interest", () => {
|
||||
it("should clear premium interest when modal is closed", async () => {
|
||||
mockPremiumInterestStateService.clearPremiumInterest.mockResolvedValue();
|
||||
|
||||
await component["onCloseClicked"]();
|
||||
|
||||
expect(mockPremiumInterestStateService.clearPremiumInterest).toHaveBeenCalledWith(
|
||||
mockAccount.id,
|
||||
);
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({ status: "closed" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("previousStep with premium interest", () => {
|
||||
it("should NOT clear premium interest when navigating between steps", async () => {
|
||||
component["step"].set(UnifiedUpgradeDialogStep.Payment);
|
||||
component["selectedPlan"].set(PersonalSubscriptionPricingTierIds.Premium);
|
||||
|
||||
await component["previousStep"]();
|
||||
|
||||
expect(mockPremiumInterestStateService.clearPremiumInterest).not.toHaveBeenCalled();
|
||||
expect(component["step"]()).toBe(UnifiedUpgradeDialogStep.PlanSelection);
|
||||
expect(component["selectedPlan"]()).toBeNull();
|
||||
});
|
||||
|
||||
it("should clear premium interest when backing out of dialog completely", async () => {
|
||||
TestBed.resetTestingModule();
|
||||
|
||||
const customDialogData: UnifiedUpgradeDialogParams = {
|
||||
account: mockAccount,
|
||||
initialStep: UnifiedUpgradeDialogStep.Payment,
|
||||
selectedPlan: PersonalSubscriptionPricingTierIds.Premium,
|
||||
};
|
||||
|
||||
mockPremiumInterestStateService.clearPremiumInterest.mockResolvedValue();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule, UnifiedUpgradeDialogComponent],
|
||||
providers: [
|
||||
{ provide: DialogRef, useValue: mockDialogRef },
|
||||
{ provide: DIALOG_DATA, useValue: customDialogData },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{ provide: PremiumInterestStateService, useValue: mockPremiumInterestStateService },
|
||||
],
|
||||
})
|
||||
.overrideComponent(UnifiedUpgradeDialogComponent, {
|
||||
remove: {
|
||||
imports: [UpgradeAccountComponent, UpgradePaymentComponent],
|
||||
},
|
||||
add: {
|
||||
imports: [MockUpgradeAccountComponent, MockUpgradePaymentComponent],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
const customFixture = TestBed.createComponent(UnifiedUpgradeDialogComponent);
|
||||
const customComponent = customFixture.componentInstance;
|
||||
customFixture.detectChanges();
|
||||
|
||||
await customComponent["previousStep"]();
|
||||
|
||||
expect(mockPremiumInterestStateService.clearPremiumInterest).toHaveBeenCalledWith(
|
||||
mockAccount.id,
|
||||
);
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({ status: "closed" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CommonModule } from "@angular/common";
|
||||
import { 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 { PersonalSubscriptionPricingTierId } from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
||||
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
||||
@@ -94,6 +95,7 @@ export class UnifiedUpgradeDialogComponent implements OnInit {
|
||||
private dialogRef: DialogRef<UnifiedUpgradeDialogResult>,
|
||||
@Inject(DIALOG_DATA) private params: UnifiedUpgradeDialogParams,
|
||||
private router: Router,
|
||||
private premiumInterestStateService: PremiumInterestStateService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -110,7 +112,9 @@ export class UnifiedUpgradeDialogComponent implements OnInit {
|
||||
this.selectedPlan.set(planId);
|
||||
this.nextStep();
|
||||
}
|
||||
protected onCloseClicked(): void {
|
||||
protected async onCloseClicked(): Promise<void> {
|
||||
// Clear premium interest when user closes/abandons modal
|
||||
await this.premiumInterestStateService.clearPremiumInterest(this.params.account.id);
|
||||
this.close({ status: UnifiedUpgradeDialogStatus.Closed });
|
||||
}
|
||||
|
||||
@@ -124,18 +128,20 @@ export class UnifiedUpgradeDialogComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
protected previousStep(): void {
|
||||
protected async previousStep(): Promise<void> {
|
||||
// If we are on the payment step and there was no initial step, go back to plan selection this is to prevent
|
||||
// going back to payment step if the dialog was opened directly to payment step
|
||||
if (this.step() === UnifiedUpgradeDialogStep.Payment && this.params?.initialStep == null) {
|
||||
this.step.set(UnifiedUpgradeDialogStep.PlanSelection);
|
||||
this.selectedPlan.set(null);
|
||||
} else {
|
||||
// Clear premium interest when backing out of dialog completely
|
||||
await this.premiumInterestStateService.clearPremiumInterest(this.params.account.id);
|
||||
this.close({ status: UnifiedUpgradeDialogStatus.Closed });
|
||||
}
|
||||
}
|
||||
|
||||
protected onComplete(result: UpgradePaymentResult): void {
|
||||
protected async onComplete(result: UpgradePaymentResult): Promise<void> {
|
||||
let status: UnifiedUpgradeDialogStatus;
|
||||
switch (result.status) {
|
||||
case "upgradedToPremium":
|
||||
@@ -153,6 +159,19 @@ export class UnifiedUpgradeDialogComponent implements OnInit {
|
||||
|
||||
this.close({ status, organizationId: result.organizationId });
|
||||
|
||||
// Check premium interest and route to vault for marketing-initiated premium upgrades
|
||||
if (status === UnifiedUpgradeDialogStatus.UpgradedToPremium) {
|
||||
const hasPremiumInterest = await this.premiumInterestStateService.getPremiumInterest(
|
||||
this.params.account.id,
|
||||
);
|
||||
if (hasPremiumInterest) {
|
||||
await this.premiumInterestStateService.clearPremiumInterest(this.params.account.id);
|
||||
await this.router.navigate(["/vault"]);
|
||||
return; // Exit early, don't use redirectOnCompletion
|
||||
}
|
||||
}
|
||||
|
||||
// Use redirectOnCompletion for standard upgrade flows
|
||||
if (
|
||||
this.params.redirectOnCompletion &&
|
||||
(status === UnifiedUpgradeDialogStatus.UpgradedToPremium ||
|
||||
@@ -162,7 +181,7 @@ export class UnifiedUpgradeDialogComponent implements OnInit {
|
||||
status === UnifiedUpgradeDialogStatus.UpgradedToFamilies
|
||||
? `/organizations/${result.organizationId}/vault`
|
||||
: "/settings/subscription/user-subscription";
|
||||
void this.router.navigate([redirectUrl]);
|
||||
await this.router.navigate([redirectUrl]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user