From ac5c73f70433c32e79e96c7ebbfec1b5b6aa8dd3 Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Wed, 4 Feb 2026 18:38:22 -0500 Subject: [PATCH] refactor(billing): update premium org upgrade payment to display existing payment method --- ...premium-org-upgrade-payment.component.html | 15 +++-- .../premium-org-upgrade-payment.component.ts | 61 ++++++++++++------- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.html b/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.html index 1df2ca13178..388c9f13ea7 100644 --- a/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.html +++ b/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.html @@ -14,13 +14,12 @@
{{ "paymentMethod" | i18n }}
- + +
{{ "billingAddress" | i18n }}
{{ "upgrade" | i18n }} diff --git a/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.ts b/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.ts index bd5964ad81b..4b35b2ae2ae 100644 --- a/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.ts +++ b/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.ts @@ -23,9 +23,11 @@ import { Observable, from, defer, + map, + tap, } from "rxjs"; -import { Account } from "@bitwarden/common/auth/abstractions/account.service"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { SubscriptionPricingServiceAbstraction } from "@bitwarden/common/billing/abstractions/subscription-pricing.service.abstraction"; import { BusinessSubscriptionPricingTier, @@ -41,11 +43,14 @@ import { LogService } from "@bitwarden/logging"; import { Cart, CartSummaryComponent } from "@bitwarden/pricing"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; +import { SubscriberBillingClient } from "../../../clients/subscriber-billing.client"; import { EnterBillingAddressComponent, - EnterPaymentMethodComponent, + DisplayPaymentMethodComponent, getBillingAddressFromForm, } from "../../../payment/components"; +import { MaskedPaymentMethod } from "../../../payment/types"; +import { BitwardenSubscriber, mapAccountToSubscriber } from "../../../types"; import { PremiumOrgUpgradeService, @@ -75,7 +80,7 @@ export type PremiumOrgUpgradePaymentResult = { SharedModule, CartSummaryComponent, ButtonModule, - EnterPaymentMethodComponent, + DisplayPaymentMethodComponent, EnterBillingAddressComponent, ], providers: [PremiumOrgUpgradeService], @@ -108,12 +113,10 @@ export class PremiumOrgUpgradePaymentComponent implements OnInit, AfterViewInit protected goBack = output(); protected complete = output(); - readonly paymentComponent = viewChild.required(EnterPaymentMethodComponent); readonly cartSummaryComponent = viewChild.required(CartSummaryComponent); protected formGroup = new FormGroup({ organizationName: new FormControl("", [Validators.required]), - paymentForm: EnterPaymentMethodComponent.getFormGroup(), billingAddress: EnterBillingAddressComponent.getFormGroup(), }); @@ -121,6 +124,10 @@ export class PremiumOrgUpgradePaymentComponent implements OnInit, AfterViewInit protected readonly loading = signal(true); protected readonly upgradeToMessage = signal(""); + // Signals for payment method + protected readonly paymentMethod = signal(null); + protected readonly subscriber = signal(null); + protected readonly planMembershipMessage = computed( () => this.PLAN_MEMBERSHIP_MESSAGES[this.selectedPlanId()] ?? "", ); @@ -144,6 +151,15 @@ export class PremiumOrgUpgradePaymentComponent implements OnInit, AfterViewInit initialValue: this.getEmptyInvoicePreview(), }); + private readonly i18nService = inject(I18nService); + private readonly subscriptionPricingService = inject(SubscriptionPricingServiceAbstraction); + private readonly toastService = inject(ToastService); + private readonly logService = inject(LogService); + private readonly destroyRef = inject(DestroyRef); + private readonly premiumOrgUpgradeService = inject(PremiumOrgUpgradeService); + private readonly subscriberBillingClient = inject(SubscriberBillingClient); + private readonly accountService = inject(AccountService); + // Cart Summary data protected readonly cart = computed(() => { if (!this.selectedPlan()) { @@ -180,13 +196,6 @@ export class PremiumOrgUpgradePaymentComponent implements OnInit, AfterViewInit }; }); - private readonly i18nService = inject(I18nService); - private readonly subscriptionPricingService = inject(SubscriptionPricingServiceAbstraction); - private readonly toastService = inject(ToastService); - private readonly logService = inject(LogService); - private readonly destroyRef = inject(DestroyRef); - private readonly premiumOrgUpgradeService = inject(PremiumOrgUpgradeService); - async ngOnInit(): Promise { // If the selected plan is Personal Premium, no upgrade is needed if (this.selectedPlanId() == PersonalSubscriptionPricingTierIds.Premium) { @@ -231,6 +240,22 @@ export class PremiumOrgUpgradePaymentComponent implements OnInit, AfterViewInit } }); + this.accountService.activeAccount$ + .pipe( + mapAccountToSubscriber, + switchMap((subscriber) => + from(this.subscriberBillingClient.getPaymentMethod(subscriber)).pipe( + map((paymentMethod) => ({ subscriber, paymentMethod })), + ), + ), + tap(({ subscriber, paymentMethod }) => { + this.subscriber.set(subscriber); + this.paymentMethod.set(paymentMethod); + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(); + this.loading.set(false); } @@ -240,7 +265,7 @@ export class PremiumOrgUpgradePaymentComponent implements OnInit, AfterViewInit } protected submit = async (): Promise => { - if (!this.isFormValid()) { + if (!this.formGroup.valid) { this.formGroup.markAllAsTouched(); return; } @@ -265,10 +290,6 @@ export class PremiumOrgUpgradePaymentComponent implements OnInit, AfterViewInit } }; - protected isFormValid(): boolean { - return this.formGroup.valid && this.paymentComponent().validate(); - } - private async processUpgrade(): Promise { const billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress); const organizationName = this.formGroup.value?.organizationName; @@ -281,12 +302,6 @@ export class PremiumOrgUpgradePaymentComponent implements OnInit, AfterViewInit throw new Error("Organization name is required"); } - const paymentMethod = await this.paymentComponent().tokenize(); - - if (!paymentMethod) { - throw new Error("Payment method is required"); - } - const organizationId = await this.premiumOrgUpgradeService.upgradeToOrganization( this.account(), organizationName,