mirror of
https://github.com/bitwarden/browser
synced 2026-02-11 05:53:42 +00:00
feat(billing): Add dedicated Premium to Organization upgrade dialog
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
<!-- Premium-to-Organization upgrade flow: User already has premium personally and is upgrading to Teams/Enterprise -->
|
||||
@if (step() == PlanSelectionStep) {
|
||||
<app-premium-org-upgrade
|
||||
(planSelected)="onPlanSelected($event)"
|
||||
(closeClicked)="onCloseClicked()"
|
||||
/>
|
||||
} @else if (step() == PaymentStep && selectedPlan() !== null && account() !== null) {
|
||||
<app-premium-org-upgrade-payment
|
||||
[selectedPlanId]="selectedPlan()"
|
||||
[account]="account()"
|
||||
(goBack)="previousStep()"
|
||||
(complete)="onComplete($event)"
|
||||
/>
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
import { ChangeDetectionStrategy, 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 { of } from "rxjs";
|
||||
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import {
|
||||
BusinessSubscriptionPricingTierId,
|
||||
PersonalSubscriptionPricingTierId,
|
||||
} 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 {
|
||||
PremiumOrgUpgradeDialogComponent,
|
||||
PremiumOrgUpgradeDialogParams,
|
||||
PremiumOrgUpgradeDialogStep,
|
||||
} from "./premium-org-upgrade-dialog.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-premium-org-upgrade",
|
||||
template: "",
|
||||
standalone: true,
|
||||
providers: [PremiumOrgUpgradeComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
class MockPremiumOrgUpgradeComponent {
|
||||
readonly dialogTitleMessageOverride = input<string | null>(null);
|
||||
readonly hideContinueWithoutUpgradingButton = input<boolean>(false);
|
||||
planSelected = output<BusinessSubscriptionPricingTierId>();
|
||||
closeClicked = output<PremiumOrgUpgradePaymentStatus>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-premium-org-upgrade-payment",
|
||||
template: "",
|
||||
standalone: true,
|
||||
providers: [PremiumOrgUpgradePaymentComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
class MockPremiumOrgUpgradePaymentComponent {
|
||||
readonly selectedPlanId = input<
|
||||
BusinessSubscriptionPricingTierId | PersonalSubscriptionPricingTierId | null
|
||||
>(null);
|
||||
readonly account = input<Account | null>(null);
|
||||
goBack = output<void>();
|
||||
complete = output<{ status: PremiumOrgUpgradePaymentStatus; organizationId: string | null }>();
|
||||
}
|
||||
|
||||
describe("PremiumOrgUpgradeDialogComponent", () => {
|
||||
let component: PremiumOrgUpgradeDialogComponent;
|
||||
let fixture: ComponentFixture<PremiumOrgUpgradeDialogComponent>;
|
||||
const mockDialogRef = mock<DialogRef>();
|
||||
const mockRouter = mock<Router>();
|
||||
const mockBillingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||
const mockConfigService = mock<ConfigService>();
|
||||
const mockAccount: Account = {
|
||||
id: "user-id" as UserId,
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
};
|
||||
|
||||
const defaultDialogData: PremiumOrgUpgradeDialogParams = {
|
||||
account: mockAccount,
|
||||
initialStep: null,
|
||||
selectedPlan: null,
|
||||
planSelectionStepTitleOverride: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to create and configure a fresh component instance with custom dialog data
|
||||
*/
|
||||
async function createComponentWithDialogData(
|
||||
dialogData: PremiumOrgUpgradeDialogParams,
|
||||
waitForStable = false,
|
||||
): Promise<{
|
||||
fixture: ComponentFixture<PremiumOrgUpgradeDialogComponent>;
|
||||
component: PremiumOrgUpgradeDialogComponent;
|
||||
}> {
|
||||
TestBed.resetTestingModule();
|
||||
jest.clearAllMocks();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule, PremiumOrgUpgradeDialogComponent],
|
||||
providers: [
|
||||
{ provide: DialogRef, useValue: mockDialogRef },
|
||||
{ provide: DIALOG_DATA, useValue: dialogData },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{
|
||||
provide: BillingAccountProfileStateService,
|
||||
useValue: mockBillingAccountProfileStateService,
|
||||
},
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
],
|
||||
})
|
||||
.overrideComponent(PremiumOrgUpgradeDialogComponent, {
|
||||
remove: {
|
||||
imports: [PremiumOrgUpgradeComponent, PremiumOrgUpgradePaymentComponent],
|
||||
},
|
||||
add: {
|
||||
imports: [MockPremiumOrgUpgradeComponent, MockPremiumOrgUpgradePaymentComponent],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
const newFixture = TestBed.createComponent(PremiumOrgUpgradeDialogComponent);
|
||||
const newComponent = newFixture.componentInstance;
|
||||
newFixture.detectChanges();
|
||||
|
||||
if (waitForStable) {
|
||||
await newFixture.whenStable();
|
||||
}
|
||||
|
||||
return { fixture: newFixture, component: newComponent };
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
// Reset mocks
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockBillingAccountProfileStateService.hasPremiumPersonally$.mockReturnValue(of(true));
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PremiumOrgUpgradeDialogComponent],
|
||||
providers: [
|
||||
{ provide: DialogRef, useValue: mockDialogRef },
|
||||
{ provide: DIALOG_DATA, useValue: defaultDialogData },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{
|
||||
provide: BillingAccountProfileStateService,
|
||||
useValue: mockBillingAccountProfileStateService,
|
||||
},
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
],
|
||||
})
|
||||
.overrideComponent(PremiumOrgUpgradeDialogComponent, {
|
||||
remove: {
|
||||
imports: [PremiumOrgUpgradeComponent, PremiumOrgUpgradePaymentComponent],
|
||||
},
|
||||
add: {
|
||||
imports: [MockPremiumOrgUpgradeComponent, MockPremiumOrgUpgradePaymentComponent],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PremiumOrgUpgradeDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should initialize with default values", () => {
|
||||
expect(component["step"]()).toBe(PremiumOrgUpgradeDialogStep.PlanSelection);
|
||||
expect(component["selectedPlan"]()).toBeNull();
|
||||
expect(component["account"]()).toEqual(mockAccount);
|
||||
expect(component["planSelectionStepTitleOverride"]()).toBeNull();
|
||||
});
|
||||
|
||||
it("should initialize with custom initial step", async () => {
|
||||
const customDialogData: PremiumOrgUpgradeDialogParams = {
|
||||
account: mockAccount,
|
||||
initialStep: PremiumOrgUpgradeDialogStep.Payment,
|
||||
selectedPlan: "teams" as BusinessSubscriptionPricingTierId,
|
||||
};
|
||||
|
||||
const { component: customComponent } = await createComponentWithDialogData(customDialogData);
|
||||
|
||||
expect(customComponent["step"]()).toBe(PremiumOrgUpgradeDialogStep.Payment);
|
||||
expect(customComponent["selectedPlan"]()).toBe("teams");
|
||||
});
|
||||
|
||||
describe("custom dialog title", () => {
|
||||
it("should use null as default when no override is provided", () => {
|
||||
expect(component["planSelectionStepTitleOverride"]()).toBeNull();
|
||||
});
|
||||
|
||||
it("should use custom title when provided in dialog config", async () => {
|
||||
const customDialogData: PremiumOrgUpgradeDialogParams = {
|
||||
account: mockAccount,
|
||||
initialStep: PremiumOrgUpgradeDialogStep.PlanSelection,
|
||||
selectedPlan: null,
|
||||
planSelectionStepTitleOverride: "upgradeYourPlan",
|
||||
};
|
||||
|
||||
const { component: customComponent } = await createComponentWithDialogData(customDialogData);
|
||||
|
||||
expect(customComponent["planSelectionStepTitleOverride"]()).toBe("upgradeYourPlan");
|
||||
});
|
||||
});
|
||||
|
||||
describe("onPlanSelected", () => {
|
||||
it("should set selected plan and move to payment step", () => {
|
||||
component["onPlanSelected"]("teams" as BusinessSubscriptionPricingTierId);
|
||||
|
||||
expect(component["selectedPlan"]()).toBe("teams");
|
||||
expect(component["step"]()).toBe(PremiumOrgUpgradeDialogStep.Payment);
|
||||
});
|
||||
|
||||
it("should handle selecting Enterprise plan", () => {
|
||||
component["onPlanSelected"]("enterprise" as BusinessSubscriptionPricingTierId);
|
||||
|
||||
expect(component["selectedPlan"]()).toBe("enterprise");
|
||||
expect(component["step"]()).toBe(PremiumOrgUpgradeDialogStep.Payment);
|
||||
});
|
||||
});
|
||||
|
||||
describe("previousStep", () => {
|
||||
it("should go back to plan selection and clear selected plan", async () => {
|
||||
component["step"].set(PremiumOrgUpgradeDialogStep.Payment);
|
||||
component["selectedPlan"].set("teams" as BusinessSubscriptionPricingTierId);
|
||||
|
||||
await component["previousStep"]();
|
||||
|
||||
expect(component["step"]()).toBe(PremiumOrgUpgradeDialogStep.PlanSelection);
|
||||
expect(component["selectedPlan"]()).toBeNull();
|
||||
});
|
||||
|
||||
it("should close dialog when backing out from initial step", async () => {
|
||||
const customDialogData: PremiumOrgUpgradeDialogParams = {
|
||||
account: mockAccount,
|
||||
initialStep: PremiumOrgUpgradeDialogStep.Payment,
|
||||
selectedPlan: "teams" as BusinessSubscriptionPricingTierId,
|
||||
};
|
||||
|
||||
const { component: customComponent } = await createComponentWithDialogData(customDialogData);
|
||||
|
||||
await customComponent["previousStep"]();
|
||||
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({ status: "closed" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("onComplete", () => {
|
||||
it("should handle completing upgrade to Families successfully", async () => {
|
||||
const { component: testComponent } = await createComponentWithDialogData(defaultDialogData);
|
||||
mockRouter.navigate.mockResolvedValue(true);
|
||||
|
||||
const result = {
|
||||
status: "upgradedToFamilies" as const,
|
||||
organizationId: "org-111",
|
||||
};
|
||||
|
||||
await testComponent["onComplete"](result);
|
||||
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({
|
||||
status: "upgradedToFamilies",
|
||||
organizationId: "org-111",
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle completing upgrade to Teams successfully", async () => {
|
||||
const { component: testComponent } = await createComponentWithDialogData(defaultDialogData);
|
||||
mockRouter.navigate.mockResolvedValue(true);
|
||||
|
||||
const result = {
|
||||
status: "upgradedToTeams" as const,
|
||||
organizationId: "org-123",
|
||||
};
|
||||
|
||||
await testComponent["onComplete"](result);
|
||||
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({
|
||||
status: "upgradedToTeams",
|
||||
organizationId: "org-123",
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle completing upgrade to Enterprise successfully", async () => {
|
||||
const { component: testComponent } = await createComponentWithDialogData(defaultDialogData);
|
||||
mockRouter.navigate.mockResolvedValue(true);
|
||||
|
||||
const result = {
|
||||
status: "upgradedToEnterprise" as const,
|
||||
organizationId: "org-456",
|
||||
};
|
||||
|
||||
await testComponent["onComplete"](result);
|
||||
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({
|
||||
status: "upgradedToEnterprise",
|
||||
organizationId: "org-456",
|
||||
});
|
||||
});
|
||||
|
||||
it("should redirect to organization vault after Teams upgrade when redirectOnCompletion is true", async () => {
|
||||
const customDialogData: PremiumOrgUpgradeDialogParams = {
|
||||
account: mockAccount,
|
||||
redirectOnCompletion: true,
|
||||
};
|
||||
|
||||
mockRouter.navigate.mockResolvedValue(true);
|
||||
|
||||
const { component: customComponent } = await createComponentWithDialogData(customDialogData);
|
||||
|
||||
const result = {
|
||||
status: "upgradedToTeams" as const,
|
||||
organizationId: "org-123",
|
||||
};
|
||||
|
||||
await customComponent["onComplete"](result);
|
||||
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(["/organizations/org-123/vault"]);
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({
|
||||
status: "upgradedToTeams",
|
||||
organizationId: "org-123",
|
||||
});
|
||||
});
|
||||
|
||||
it("should redirect to organization vault after Enterprise upgrade when redirectOnCompletion is true", async () => {
|
||||
const customDialogData: PremiumOrgUpgradeDialogParams = {
|
||||
account: mockAccount,
|
||||
redirectOnCompletion: true,
|
||||
};
|
||||
|
||||
mockRouter.navigate.mockResolvedValue(true);
|
||||
|
||||
const { component: customComponent } = await createComponentWithDialogData(customDialogData);
|
||||
|
||||
const result = {
|
||||
status: "upgradedToEnterprise" as const,
|
||||
organizationId: "org-789",
|
||||
};
|
||||
|
||||
await customComponent["onComplete"](result);
|
||||
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(["/organizations/org-789/vault"]);
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({
|
||||
status: "upgradedToEnterprise",
|
||||
organizationId: "org-789",
|
||||
});
|
||||
});
|
||||
|
||||
it("should redirect to organization vault after Families upgrade when redirectOnCompletion is true", async () => {
|
||||
const customDialogData: PremiumOrgUpgradeDialogParams = {
|
||||
account: mockAccount,
|
||||
redirectOnCompletion: true,
|
||||
};
|
||||
|
||||
mockRouter.navigate.mockResolvedValue(true);
|
||||
|
||||
const { component: customComponent } = await createComponentWithDialogData(customDialogData);
|
||||
|
||||
const result = {
|
||||
status: "upgradedToFamilies" as const,
|
||||
organizationId: "org-999",
|
||||
};
|
||||
|
||||
await customComponent["onComplete"](result);
|
||||
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(["/organizations/org-999/vault"]);
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({
|
||||
status: "upgradedToFamilies",
|
||||
organizationId: "org-999",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not redirect when redirectOnCompletion is false", async () => {
|
||||
const customDialogData: PremiumOrgUpgradeDialogParams = {
|
||||
account: mockAccount,
|
||||
redirectOnCompletion: false,
|
||||
};
|
||||
|
||||
const { component: customComponent } = await createComponentWithDialogData(customDialogData);
|
||||
|
||||
const result = {
|
||||
status: "upgradedToTeams" as const,
|
||||
organizationId: "org-123",
|
||||
};
|
||||
|
||||
await customComponent["onComplete"](result);
|
||||
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalled();
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({
|
||||
status: "upgradedToTeams",
|
||||
organizationId: "org-123",
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle closed status", async () => {
|
||||
const { component: testComponent } = await createComponentWithDialogData(defaultDialogData);
|
||||
|
||||
const result: PremiumOrgUpgradePaymentResult = { status: "closed", organizationId: null };
|
||||
|
||||
await testComponent["onComplete"](result);
|
||||
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({
|
||||
status: "closed",
|
||||
organizationId: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("onCloseClicked", () => {
|
||||
it("should close dialog", async () => {
|
||||
await component["onCloseClicked"]();
|
||||
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({ status: "closed" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Child Component Display Logic", () => {
|
||||
describe("Plan Selection Step", () => {
|
||||
it("should display app-premium-org-upgrade on plan selection step", async () => {
|
||||
const { fixture } = await createComponentWithDialogData(defaultDialogData);
|
||||
|
||||
const premiumOrgUpgradeElement =
|
||||
fixture.nativeElement.querySelector("app-premium-org-upgrade");
|
||||
|
||||
expect(premiumOrgUpgradeElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Payment Step", () => {
|
||||
it("should display app-premium-org-upgrade-payment on payment step", async () => {
|
||||
const customDialogData: PremiumOrgUpgradeDialogParams = {
|
||||
account: mockAccount,
|
||||
initialStep: PremiumOrgUpgradeDialogStep.Payment,
|
||||
selectedPlan: "teams" as BusinessSubscriptionPricingTierId,
|
||||
};
|
||||
|
||||
const { fixture } = await createComponentWithDialogData(customDialogData);
|
||||
|
||||
const premiumOrgUpgradePaymentElement = fixture.nativeElement.querySelector(
|
||||
"app-premium-org-upgrade-payment",
|
||||
);
|
||||
|
||||
expect(premiumOrgUpgradePaymentElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,210 @@
|
||||
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 { Router } from "@angular/router";
|
||||
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
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,
|
||||
DialogConfig,
|
||||
DialogModule,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
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 { UpgradePaymentService } from "../upgrade-payment/services/upgrade-payment.service";
|
||||
|
||||
export const PremiumOrgUpgradeDialogStatus = {
|
||||
Closed: "closed",
|
||||
UpgradedToFamilies: "upgradedToFamilies",
|
||||
UpgradedToTeams: "upgradedToTeams",
|
||||
UpgradedToEnterprise: "upgradedToEnterprise",
|
||||
} as const;
|
||||
|
||||
export const PremiumOrgUpgradeDialogStep = {
|
||||
PlanSelection: "planSelection",
|
||||
Payment: "payment",
|
||||
} as const;
|
||||
|
||||
export type PremiumOrgUpgradeDialogStatus = UnionOfValues<typeof PremiumOrgUpgradeDialogStatus>;
|
||||
export type PremiumOrgUpgradeDialogStep = UnionOfValues<typeof PremiumOrgUpgradeDialogStep>;
|
||||
|
||||
export type PremiumOrgUpgradeDialogResult = {
|
||||
status: PremiumOrgUpgradeDialogStatus;
|
||||
organizationId?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parameters for the PremiumOrgUpgradeDialog component.
|
||||
* In order to open the dialog to a specific step, you must provide the `initialStep` parameter and a `selectedPlan` if the step is `Payment`.
|
||||
*
|
||||
* @property {Account} account - The user account information.
|
||||
* @property {PremiumOrgUpgradeDialogStep | null} PersonalSubscriptionPricingTierId | null} [selectedPlan] - Pre-selected subscription plan (Families, Teams, or Enterprise)y.
|
||||
* @property {BusinessSubscriptionPricingTierId | null} [selectedPlan] - Pre-selected subscription plan, if any.
|
||||
* @property {string | null} [dialogTitleMessageOverride] - Optional custom i18n key to override the default dialog title.
|
||||
* @property {boolean} [hideContinueWithoutUpgradingButton] - Whether to hide the "Continue without upgrading" button.
|
||||
* @property {boolean} [redirectOnCompletion] - Whether to redirect after successful upgrade to organization vault.
|
||||
*/
|
||||
export type PremiumOrgUpgradeDialogParams = {
|
||||
account: Account;
|
||||
initialStep?: PremiumOrgUpgradeDialogStep | null;
|
||||
selectedPlan?: BusinessSubscriptionPricingTierId | PersonalSubscriptionPricingTierId | null;
|
||||
redirectOnCompletion?: boolean;
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "app-premium-org-upgrade-dialog",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
DialogModule,
|
||||
ButtonModule,
|
||||
BillingServicesModule,
|
||||
PremiumOrgUpgradeComponent,
|
||||
PremiumOrgUpgradePaymentComponent,
|
||||
],
|
||||
providers: [UpgradePaymentService, AccountBillingClient, PreviewInvoiceClient],
|
||||
templateUrl: "./premium-org-upgrade-dialog.component.html",
|
||||
})
|
||||
export class PremiumOrgUpgradeDialogComponent implements OnInit {
|
||||
// Use signals for dialog state because inputs depend on parent component
|
||||
protected readonly step = signal<PremiumOrgUpgradeDialogStep>(
|
||||
PremiumOrgUpgradeDialogStep.PlanSelection,
|
||||
);
|
||||
protected readonly selectedPlan = signal<
|
||||
BusinessSubscriptionPricingTierId | PersonalSubscriptionPricingTierId | null
|
||||
>(null);
|
||||
protected readonly account = signal<Account | null>(null);
|
||||
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 isPremiumOrgUpgradeEnabled = computed(
|
||||
() => this.hasPremiumPersonally() && this.premiumToOrganizationUpgradeEnabled(),
|
||||
);
|
||||
|
||||
protected readonly PaymentStep = PremiumOrgUpgradeDialogStep.Payment;
|
||||
protected readonly PlanSelectionStep = PremiumOrgUpgradeDialogStep.PlanSelection;
|
||||
|
||||
constructor(
|
||||
private dialogRef: DialogRef<PremiumOrgUpgradeDialogResult>,
|
||||
@Inject(DIALOG_DATA) private params: PremiumOrgUpgradeDialogParams,
|
||||
private router: Router,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.account.set(this.params.account);
|
||||
this.step.set(this.params.initialStep ?? PremiumOrgUpgradeDialogStep.PlanSelection);
|
||||
this.selectedPlan.set(this.params.selectedPlan ?? null);
|
||||
}
|
||||
|
||||
protected onPlanSelected(
|
||||
planId: BusinessSubscriptionPricingTierId | PersonalSubscriptionPricingTierId,
|
||||
): void {
|
||||
this.selectedPlan.set(planId);
|
||||
this.nextStep();
|
||||
}
|
||||
|
||||
protected async onCloseClicked(): Promise<void> {
|
||||
this.close({ status: PremiumOrgUpgradeDialogStatus.Closed });
|
||||
}
|
||||
|
||||
private close(result: PremiumOrgUpgradeDialogResult): void {
|
||||
this.dialogRef.close(result);
|
||||
}
|
||||
|
||||
protected nextStep() {
|
||||
if (this.step() === PremiumOrgUpgradeDialogStep.PlanSelection) {
|
||||
this.step.set(PremiumOrgUpgradeDialogStep.Payment);
|
||||
}
|
||||
}
|
||||
|
||||
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() === PremiumOrgUpgradeDialogStep.Payment && this.params?.initialStep == null) {
|
||||
this.step.set(PremiumOrgUpgradeDialogStep.PlanSelection);
|
||||
this.selectedPlan.set(null);
|
||||
} else {
|
||||
this.close({ status: PremiumOrgUpgradeDialogStatus.Closed });
|
||||
}
|
||||
}
|
||||
|
||||
protected async onComplete(result: PremiumOrgUpgradePaymentResult): Promise<void> {
|
||||
let status: PremiumOrgUpgradeDialogStatus;
|
||||
switch (result.status) {
|
||||
case "upgradedToFamilies":
|
||||
status = PremiumOrgUpgradeDialogStatus.UpgradedToFamilies;
|
||||
break;
|
||||
case "upgradedToTeams":
|
||||
status = PremiumOrgUpgradeDialogStatus.UpgradedToTeams;
|
||||
break;
|
||||
case "upgradedToEnterprise":
|
||||
status = PremiumOrgUpgradeDialogStatus.UpgradedToEnterprise;
|
||||
break;
|
||||
case "closed":
|
||||
status = PremiumOrgUpgradeDialogStatus.Closed;
|
||||
break;
|
||||
default:
|
||||
status = PremiumOrgUpgradeDialogStatus.Closed;
|
||||
}
|
||||
|
||||
this.close({ status, organizationId: result.organizationId });
|
||||
|
||||
// Redirect to organization vault after successful upgrade
|
||||
if (
|
||||
this.params.redirectOnCompletion &&
|
||||
(status === PremiumOrgUpgradeDialogStatus.UpgradedToFamilies ||
|
||||
status === PremiumOrgUpgradeDialogStatus.UpgradedToEnterprise ||
|
||||
status === PremiumOrgUpgradeDialogStatus.UpgradedToTeams)
|
||||
) {
|
||||
const redirectUrl = `/organizations/${result.organizationId}/vault`;
|
||||
await this.router.navigate([redirectUrl]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the premium org upgrade dialog.
|
||||
*
|
||||
* @param dialogService - The dialog service used to open the component
|
||||
* @param dialogConfig - The configuration for the dialog including PremiumOrgUpgradeDialogParams data
|
||||
* @returns A dialog reference object of type DialogRef<PremiumOrgUpgradeDialogResult>
|
||||
*/
|
||||
static open(
|
||||
dialogService: DialogService,
|
||||
dialogConfig: DialogConfig<PremiumOrgUpgradeDialogParams>,
|
||||
): DialogRef<PremiumOrgUpgradeDialogResult> {
|
||||
return dialogService.open<PremiumOrgUpgradeDialogResult>(PremiumOrgUpgradeDialogComponent, {
|
||||
data: dialogConfig.data,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user