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",
+ });
+ }
+}