diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment-dialog/upgrade-payment-dialog.component.html b/apps/web/src/app/billing/individual/upgrade/upgrade-payment-dialog/upgrade-payment-dialog.component.html
new file mode 100644
index 00000000000..add15ef7555
--- /dev/null
+++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment-dialog/upgrade-payment-dialog.component.html
@@ -0,0 +1,42 @@
+
diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment-dialog/upgrade-payment-dialog.component.spec.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-payment-dialog/upgrade-payment-dialog.component.spec.ts
new file mode 100644
index 00000000000..32bcadc9ae6
--- /dev/null
+++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment-dialog/upgrade-payment-dialog.component.spec.ts
@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+
+import { UpgradePaymentDialogComponent } from "./upgrade-payment-dialog.component";
+
+describe("UpgradePaymentDialogComponent", () => {
+ let component: UpgradePaymentDialogComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [UpgradePaymentDialogComponent],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(UpgradePaymentDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment-dialog/upgrade-payment-dialog.component.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-payment-dialog/upgrade-payment-dialog.component.ts
new file mode 100644
index 00000000000..f330ca8a51c
--- /dev/null
+++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment-dialog/upgrade-payment-dialog.component.ts
@@ -0,0 +1,290 @@
+import { DialogConfig } from "@angular/cdk/dialog";
+import { Component, DestroyRef, Inject, OnInit, ViewChild } from "@angular/core";
+import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
+import { FormControl, FormGroup, Validators } from "@angular/forms";
+import { debounceTime, Observable } from "rxjs";
+
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
+import {
+ ButtonModule,
+ DIALOG_DATA,
+ DialogModule,
+ DialogRef,
+ DialogService,
+ ToastService,
+} from "@bitwarden/components";
+import { LogService } from "@bitwarden/logging";
+import { CartSummaryComponent, LineItem } from "@bitwarden/pricing";
+import { SharedModule } from "@bitwarden/web-vault/app/shared";
+
+import { EnterPaymentMethodComponent } from "../../../payment/components";
+import { BillingServicesModule } from "../../../services";
+import { SubscriptionPricingService } from "../../../services/subscription-pricing.service";
+import { BitwardenSubscriber } from "../../../types";
+import {
+ PersonalSubscriptionPricingTier,
+ PersonalSubscriptionPricingTierId,
+ PersonalSubscriptionPricingTierIds,
+} from "../../../types/subscription-pricing-tier";
+
+import { PlanDetails, UpgradePaymentService } from "./services/upgrade-payment.service";
+
+/**
+ * Status types for upgrade payment dialog
+ */
+export const UpgradePaymentDialogResult = {
+ Back: "back",
+ UpgradedToPremium: "upgradedToPremium",
+ UpgradedToFamilies: "upgradedToFamilies",
+} as const;
+
+export type UpgradePaymentDialogResult = UnionOfValues;
+
+/**
+ * Parameters for upgrade payment dialog
+ */
+export type UpgradePaymentDialogParams = {
+ plan: PersonalSubscriptionPricingTierId | null;
+ subscriber: BitwardenSubscriber;
+};
+
+@Component({
+ selector: "app-upgrade-payment-dialog",
+ imports: [
+ DialogModule,
+ SharedModule,
+ CartSummaryComponent,
+ ButtonModule,
+ EnterPaymentMethodComponent,
+ BillingServicesModule,
+ ],
+ providers: [UpgradePaymentService],
+ templateUrl: "./upgrade-payment-dialog.component.html",
+})
+export class UpgradePaymentDialogComponent implements OnInit {
+ @ViewChild(EnterPaymentMethodComponent) paymentComponent!: EnterPaymentMethodComponent;
+
+ protected formGroup = new FormGroup({
+ organizationName: new FormControl("", [Validators.required]),
+ paymentForm: EnterPaymentMethodComponent.getFormGroup(),
+ });
+
+ protected loading = true;
+ private pricingTiers$!: Observable;
+ protected selectedPlan!: PlanDetails;
+
+ // Cart Summary data
+ protected passwordManager!: LineItem;
+ protected estimatedTax = 0;
+
+ // Display data
+ protected upgradeToMessage = "";
+
+ constructor(
+ private dialogRef: DialogRef,
+ private i18nService: I18nService,
+ private subscriptionPricingService: SubscriptionPricingService,
+ private toastService: ToastService,
+ private logService: LogService,
+ private destroyRef: DestroyRef,
+ private upgradePaymentService: UpgradePaymentService,
+ @Inject(DIALOG_DATA) private dialogParams: UpgradePaymentDialogParams,
+ ) {}
+
+ async ngOnInit(): Promise {
+ if (!this.isFamiliesPlan) {
+ this.formGroup.controls.organizationName.disable();
+ }
+
+ this.pricingTiers$ = this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$();
+ this.pricingTiers$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((plans) => {
+ const planDetails = plans.find((plan) => plan.id === this.dialogParams.plan);
+
+ if (planDetails && this.dialogParams.plan) {
+ this.selectedPlan = {
+ tier: this.dialogParams.plan,
+ details: planDetails,
+ };
+ }
+ });
+
+ if (!this.selectedPlan) {
+ this.close(UpgradePaymentDialogResult.Back);
+ return;
+ }
+
+ this.passwordManager = {
+ name: this.isFamiliesPlan ? "familiesMembership" : "premiumMembership",
+ cost: this.selectedPlan.details.passwordManager.annualPrice,
+ quantity: 1,
+ cadence: "year",
+ };
+
+ this.upgradeToMessage = this.i18nService.t(
+ this.isFamiliesPlan ? "upgradeToFamilies" : "upgradeToPremium",
+ );
+
+ this.estimatedTax = 0;
+
+ this.formGroup.valueChanges
+ .pipe(debounceTime(1000), takeUntilDestroyed(this.destroyRef))
+ .subscribe(() => this.refreshSalesTax());
+
+ this.loading = false;
+ }
+
+ protected get isPremiumPlan(): boolean {
+ return this.dialogParams.plan === PersonalSubscriptionPricingTierIds.Premium;
+ }
+
+ protected get isFamiliesPlan(): boolean {
+ return this.dialogParams.plan === PersonalSubscriptionPricingTierIds.Families;
+ }
+
+ back = () => {
+ this.close(UpgradePaymentDialogResult.Back);
+ };
+
+ static open(
+ dialogService: DialogService,
+ dialogConfig: DialogConfig,
+ ): DialogRef {
+ return dialogService.open(
+ UpgradePaymentDialogComponent,
+ dialogConfig,
+ );
+ }
+
+ protected submit = async (): Promise => {
+ if (!this.isFormValid()) {
+ this.formGroup.markAllAsTouched();
+ return;
+ }
+
+ if (!this.selectedPlan) {
+ throw new Error("No plan selected");
+ }
+
+ if (!this.formGroup.value.paymentForm?.billingAddress) {
+ throw new Error("No billing address provided");
+ }
+
+ try {
+ await (this.isFamiliesPlan ? this.processFamiliesUpgrade() : this.processPremiumUpgrade());
+ } catch (error: unknown) {
+ this.logService.error("Upgrade failed:", error);
+ this.toastService.showToast({
+ variant: "error",
+ message: this.i18nService.t("upgradeError"),
+ });
+ }
+ };
+
+ private isFormValid(): boolean {
+ return this.formGroup.valid && this.paymentComponent?.validate();
+ }
+
+ private async processFamiliesUpgrade(): Promise {
+ const organizationName = this.formGroup.value?.organizationName;
+ const country = this.formGroup.value?.paymentForm?.billingAddress?.country;
+ const postalCode = this.formGroup.value?.paymentForm?.billingAddress?.postalCode;
+
+ if (!organizationName) {
+ throw new Error("Organization name is required");
+ }
+
+ if (!country || !postalCode) {
+ throw new Error("Billing address is incomplete");
+ }
+
+ const tokenizedPaymentMethod = await this.paymentComponent.tokenize();
+ if (!tokenizedPaymentMethod) {
+ throw new Error("Payment information is incomplete");
+ }
+
+ const paymentFormValues = {
+ organizationName,
+ billingAddress: {
+ country,
+ postalCode,
+ },
+ };
+
+ await this.upgradePaymentService.upgradeToFamilies(
+ this.dialogParams.subscriber,
+ this.selectedPlan,
+ tokenizedPaymentMethod,
+ paymentFormValues,
+ );
+
+ this.toastService.showToast({
+ variant: "success",
+ message: this.i18nService.t("familiesUpdated"),
+ });
+
+ this.close(UpgradePaymentDialogResult.UpgradedToFamilies);
+ }
+
+ private async processPremiumUpgrade(): Promise {
+ const tokenizedPaymentMethod = await this.paymentComponent.tokenize();
+ if (!tokenizedPaymentMethod) {
+ throw new Error("Payment information is incomplete");
+ }
+ const country = this.formGroup.value?.paymentForm?.billingAddress?.country;
+ const postalCode = this.formGroup.value?.paymentForm?.billingAddress?.postalCode;
+
+ if (!country || !postalCode) {
+ throw new Error("Billing address is incomplete");
+ }
+
+ await this.upgradePaymentService.upgradeToPremium(
+ this.dialogParams.subscriber,
+ tokenizedPaymentMethod,
+ {
+ country,
+ postalCode,
+ },
+ );
+
+ this.toastService.showToast({
+ variant: "success",
+ message: this.i18nService.t("premiumUpdated"),
+ });
+
+ this.close(UpgradePaymentDialogResult.UpgradedToPremium);
+ }
+
+ private close(result: UpgradePaymentDialogResult) {
+ this.dialogRef.close(result);
+ }
+
+ private async refreshSalesTax(): Promise {
+ const billingAddress = {
+ country: this.formGroup.value.paymentForm?.billingAddress?.country,
+ postalCode: this.formGroup.value.paymentForm?.billingAddress?.postalCode,
+ };
+
+ if (!this.selectedPlan || !billingAddress.country || !billingAddress.postalCode) {
+ this.estimatedTax = 0;
+ return;
+ }
+
+ this.upgradePaymentService
+ .calculateEstimatedTax(this.selectedPlan, {
+ country: billingAddress.country,
+ postalCode: billingAddress.postalCode,
+ })
+ .then((tax) => {
+ this.estimatedTax = tax;
+ })
+ .catch((error: unknown) => {
+ this.logService.error("Tax calculation failed:", error);
+ this.toastService.showToast({
+ variant: "error",
+ message: this.i18nService.t("taxCalculationError"),
+ });
+ this.estimatedTax = 0;
+ });
+ }
+}