From ab44825ecf53bb46f6c1d8003833e092b9df9fdb Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Wed, 1 Oct 2025 18:38:21 -0400 Subject: [PATCH] fix(billing): Add unified upgrade dialog component --- .../unified-upgrade-dialog.component.html | 10 ++ .../unified-upgrade-dialog.component.ts | 150 ++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.html create mode 100644 apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.ts 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 new file mode 100644 index 00000000000..6cffd818afc --- /dev/null +++ b/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.html @@ -0,0 +1,10 @@ +@if (step() == PlanSelectionStep) { + +} @else if (step() == PaymentStep && selectedPlan() !== null) { + +} 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 new file mode 100644 index 00000000000..40972e0b6a5 --- /dev/null +++ b/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.ts @@ -0,0 +1,150 @@ +import { DIALOG_DATA } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, Inject, OnInit, signal } from "@angular/core"; + +import { Account } from "@bitwarden/common/auth/abstractions/account.service"; +import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; +import { + ButtonModule, + DialogConfig, + DialogModule, + DialogRef, + DialogService, +} from "@bitwarden/components"; + +import { AccountBillingClient } from "../../../clients"; +import { BillingServicesModule } from "../../../services"; +import { PersonalSubscriptionPricingTierId } from "../../../types/subscription-pricing-tier"; +import { UpgradeAccountComponent } from "../upgrade-account/upgrade-account.component"; +import { UpgradePaymentService } from "../upgrade-payment/services/upgrade-payment.service"; +import { + UpgradePaymentComponent, + UpgradePaymentResult, +} from "../upgrade-payment/upgrade-payment.component"; + +export const UnifiedUpgradeDialogStatus = { + Closed: "closed", + UpgradedToPremium: "upgradedToPremium", + UpgradedToFamilies: "upgradedToFamilies", +} as const; + +export const UnifiedUpgradeDialogStep = { + PlanSelection: "planSelection", + Payment: "payment", +} as const; + +export type UnifiedUpgradeDialogStatus = UnionOfValues; +export type UnifiedUpgradeDialogStep = UnionOfValues; + +export type UnifiedUpgradeDialogResult = { + status: UnifiedUpgradeDialogStatus; + organizationId?: string | null; +}; + +/** + * Parameters for the UnifiedUpgradeDialog 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 {UnifiedUpgradeDialogStep | null} [initialStep] - The initial step to show in the dialog, if any. + * @property {PersonalSubscriptionPricingTierId | null} [selectedPlan] - Pre-selected subscription plan, if any. + */ +export type UnifiedUpgradeDialogParams = { + account: Account; + initialStep?: UnifiedUpgradeDialogStep | null; + selectedPlan?: PersonalSubscriptionPricingTierId | null; +}; + +@Component({ + selector: "app-unified-upgrade-dialog", + imports: [ + CommonModule, + DialogModule, + ButtonModule, + UpgradeAccountComponent, + UpgradePaymentComponent, + BillingServicesModule, + ], + providers: [UpgradePaymentService, AccountBillingClient], + templateUrl: "./unified-upgrade-dialog.component.html", +}) +export class UnifiedUpgradeDialogComponent implements OnInit { + // Use signals for dialog state because inputs depend on parent component + protected step = signal(UnifiedUpgradeDialogStep.PlanSelection); + protected selectedPlan = signal(null); + protected account = signal(null); + + protected readonly PaymentStep = UnifiedUpgradeDialogStep.Payment; + protected readonly PlanSelectionStep = UnifiedUpgradeDialogStep.PlanSelection; + + constructor( + private dialogRef: DialogRef, + @Inject(DIALOG_DATA) private params: UnifiedUpgradeDialogParams, + ) {} + + ngOnInit(): void { + this.account.set(this.params.account); + this.step.set(this.params.initialStep ?? UnifiedUpgradeDialogStep.PlanSelection); + this.selectedPlan.set(this.params.selectedPlan ?? null); + } + + protected onPlanSelected(planId: PersonalSubscriptionPricingTierId): void { + this.selectedPlan.set(planId); + this.nextStep(); + } + protected onCloseClicked(): void { + this.close({ status: UnifiedUpgradeDialogStatus.Closed }); + } + + private close(result: UnifiedUpgradeDialogResult): void { + this.dialogRef.close(result); + } + + protected nextStep() { + if (this.step() === UnifiedUpgradeDialogStep.PlanSelection) { + this.step.set(UnifiedUpgradeDialogStep.Payment); + } + } + + protected previousStep(): void { + if (this.step() === UnifiedUpgradeDialogStep.Payment) { + this.step.set(UnifiedUpgradeDialogStep.PlanSelection); + this.selectedPlan.set(null); + } + } + + protected onComplete(result: UpgradePaymentResult): void { + let status: UnifiedUpgradeDialogStatus; + switch (result.status) { + case "upgradedToPremium": + status = UnifiedUpgradeDialogStatus.UpgradedToPremium; + break; + case "upgradedToFamilies": + status = UnifiedUpgradeDialogStatus.UpgradedToFamilies; + break; + case "closed": + status = UnifiedUpgradeDialogStatus.Closed; + break; + default: + status = UnifiedUpgradeDialogStatus.Closed; + } + this.close({ status, organizationId: result.organizationId }); + } + + /** + * Opens the unified upgrade dialog. + * + * @param dialogService - The dialog service used to open the component + * @param dialogConfig - The configuration for the dialog including UnifiedUpgradeDialogParams data + * @returns A dialog reference object of type DialogRef + */ + static open( + dialogService: DialogService, + dialogConfig: DialogConfig, + ): DialogRef { + return dialogService.open(UnifiedUpgradeDialogComponent, { + data: dialogConfig.data, + height: "auto", + }); + } +}