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 { Component, input, output } from "@angular/core";
|
||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
import { mock } from "jest-mock-extended";
|
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 { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import {
|
import {
|
||||||
PersonalSubscriptionPricingTierId,
|
PersonalSubscriptionPricingTierId,
|
||||||
@@ -58,6 +60,8 @@ describe("UnifiedUpgradeDialogComponent", () => {
|
|||||||
let component: UnifiedUpgradeDialogComponent;
|
let component: UnifiedUpgradeDialogComponent;
|
||||||
let fixture: ComponentFixture<UnifiedUpgradeDialogComponent>;
|
let fixture: ComponentFixture<UnifiedUpgradeDialogComponent>;
|
||||||
const mockDialogRef = mock<DialogRef>();
|
const mockDialogRef = mock<DialogRef>();
|
||||||
|
const mockRouter = mock<Router>();
|
||||||
|
const mockPremiumInterestStateService = mock<PremiumInterestStateService>();
|
||||||
|
|
||||||
const mockAccount: Account = {
|
const mockAccount: Account = {
|
||||||
id: "user-id" as UserId,
|
id: "user-id" as UserId,
|
||||||
@@ -74,11 +78,16 @@ describe("UnifiedUpgradeDialogComponent", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
// Reset mocks
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [NoopAnimationsModule, UnifiedUpgradeDialogComponent],
|
imports: [NoopAnimationsModule, UnifiedUpgradeDialogComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: DialogRef, useValue: mockDialogRef },
|
{ provide: DialogRef, useValue: mockDialogRef },
|
||||||
{ provide: DIALOG_DATA, useValue: defaultDialogData },
|
{ provide: DIALOG_DATA, useValue: defaultDialogData },
|
||||||
|
{ provide: Router, useValue: mockRouter },
|
||||||
|
{ provide: PremiumInterestStateService, useValue: mockPremiumInterestStateService },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.overrideComponent(UnifiedUpgradeDialogComponent, {
|
.overrideComponent(UnifiedUpgradeDialogComponent, {
|
||||||
@@ -121,6 +130,8 @@ describe("UnifiedUpgradeDialogComponent", () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: DialogRef, useValue: mockDialogRef },
|
{ provide: DialogRef, useValue: mockDialogRef },
|
||||||
{ provide: DIALOG_DATA, useValue: customDialogData },
|
{ provide: DIALOG_DATA, useValue: customDialogData },
|
||||||
|
{ provide: Router, useValue: mockRouter },
|
||||||
|
{ provide: PremiumInterestStateService, useValue: mockPremiumInterestStateService },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.overrideComponent(UnifiedUpgradeDialogComponent, {
|
.overrideComponent(UnifiedUpgradeDialogComponent, {
|
||||||
@@ -161,6 +172,8 @@ describe("UnifiedUpgradeDialogComponent", () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: DialogRef, useValue: mockDialogRef },
|
{ provide: DialogRef, useValue: mockDialogRef },
|
||||||
{ provide: DIALOG_DATA, useValue: customDialogData },
|
{ provide: DIALOG_DATA, useValue: customDialogData },
|
||||||
|
{ provide: Router, useValue: mockRouter },
|
||||||
|
{ provide: PremiumInterestStateService, useValue: mockPremiumInterestStateService },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.overrideComponent(UnifiedUpgradeDialogComponent, {
|
.overrideComponent(UnifiedUpgradeDialogComponent, {
|
||||||
@@ -191,11 +204,11 @@ describe("UnifiedUpgradeDialogComponent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("previousStep", () => {
|
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["step"].set(UnifiedUpgradeDialogStep.Payment);
|
||||||
component["selectedPlan"].set(PersonalSubscriptionPricingTierIds.Premium);
|
component["selectedPlan"].set(PersonalSubscriptionPricingTierIds.Premium);
|
||||||
|
|
||||||
component["previousStep"]();
|
await component["previousStep"]();
|
||||||
|
|
||||||
expect(component["step"]()).toBe(UnifiedUpgradeDialogStep.PlanSelection);
|
expect(component["step"]()).toBe(UnifiedUpgradeDialogStep.PlanSelection);
|
||||||
expect(component["selectedPlan"]()).toBeNull();
|
expect(component["selectedPlan"]()).toBeNull();
|
||||||
@@ -222,6 +235,8 @@ describe("UnifiedUpgradeDialogComponent", () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: DialogRef, useValue: mockDialogRef },
|
{ provide: DialogRef, useValue: mockDialogRef },
|
||||||
{ provide: DIALOG_DATA, useValue: customDialogData },
|
{ provide: DIALOG_DATA, useValue: customDialogData },
|
||||||
|
{ provide: Router, useValue: mockRouter },
|
||||||
|
{ provide: PremiumInterestStateService, useValue: mockPremiumInterestStateService },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.overrideComponent(UnifiedUpgradeDialogComponent, {
|
.overrideComponent(UnifiedUpgradeDialogComponent, {
|
||||||
@@ -241,4 +256,169 @@ describe("UnifiedUpgradeDialogComponent", () => {
|
|||||||
expect(customComponent["hideContinueWithoutUpgradingButton"]()).toBe(true);
|
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 { Component, Inject, OnInit, signal } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
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 { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { PersonalSubscriptionPricingTierId } from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
import { PersonalSubscriptionPricingTierId } from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
||||||
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
||||||
@@ -94,6 +95,7 @@ export class UnifiedUpgradeDialogComponent implements OnInit {
|
|||||||
private dialogRef: DialogRef<UnifiedUpgradeDialogResult>,
|
private dialogRef: DialogRef<UnifiedUpgradeDialogResult>,
|
||||||
@Inject(DIALOG_DATA) private params: UnifiedUpgradeDialogParams,
|
@Inject(DIALOG_DATA) private params: UnifiedUpgradeDialogParams,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
|
private premiumInterestStateService: PremiumInterestStateService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -110,7 +112,9 @@ export class UnifiedUpgradeDialogComponent implements OnInit {
|
|||||||
this.selectedPlan.set(planId);
|
this.selectedPlan.set(planId);
|
||||||
this.nextStep();
|
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 });
|
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
|
// 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
|
// going back to payment step if the dialog was opened directly to payment step
|
||||||
if (this.step() === UnifiedUpgradeDialogStep.Payment && this.params?.initialStep == null) {
|
if (this.step() === UnifiedUpgradeDialogStep.Payment && this.params?.initialStep == null) {
|
||||||
this.step.set(UnifiedUpgradeDialogStep.PlanSelection);
|
this.step.set(UnifiedUpgradeDialogStep.PlanSelection);
|
||||||
this.selectedPlan.set(null);
|
this.selectedPlan.set(null);
|
||||||
} else {
|
} else {
|
||||||
|
// Clear premium interest when backing out of dialog completely
|
||||||
|
await this.premiumInterestStateService.clearPremiumInterest(this.params.account.id);
|
||||||
this.close({ status: UnifiedUpgradeDialogStatus.Closed });
|
this.close({ status: UnifiedUpgradeDialogStatus.Closed });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onComplete(result: UpgradePaymentResult): void {
|
protected async onComplete(result: UpgradePaymentResult): Promise<void> {
|
||||||
let status: UnifiedUpgradeDialogStatus;
|
let status: UnifiedUpgradeDialogStatus;
|
||||||
switch (result.status) {
|
switch (result.status) {
|
||||||
case "upgradedToPremium":
|
case "upgradedToPremium":
|
||||||
@@ -153,6 +159,19 @@ export class UnifiedUpgradeDialogComponent implements OnInit {
|
|||||||
|
|
||||||
this.close({ status, organizationId: result.organizationId });
|
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 (
|
if (
|
||||||
this.params.redirectOnCompletion &&
|
this.params.redirectOnCompletion &&
|
||||||
(status === UnifiedUpgradeDialogStatus.UpgradedToPremium ||
|
(status === UnifiedUpgradeDialogStatus.UpgradedToPremium ||
|
||||||
@@ -162,7 +181,7 @@ export class UnifiedUpgradeDialogComponent implements OnInit {
|
|||||||
status === UnifiedUpgradeDialogStatus.UpgradedToFamilies
|
status === UnifiedUpgradeDialogStatus.UpgradedToFamilies
|
||||||
? `/organizations/${result.organizationId}/vault`
|
? `/organizations/${result.organizationId}/vault`
|
||||||
: "/settings/subscription/user-subscription";
|
: "/settings/subscription/user-subscription";
|
||||||
void this.router.navigate([redirectUrl]);
|
await this.router.navigate([redirectUrl]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user