1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-11 05:53:42 +00:00

refactor(billing): update premium org upgrade payment to display existing payment method

This commit is contained in:
Stephon Brown
2026-02-04 18:38:22 -05:00
parent 249ac98bd0
commit ac5c73f704
2 changed files with 45 additions and 31 deletions

View File

@@ -14,13 +14,12 @@
</div>
<div class="tw-pb-8 !tw-mx-0">
<h5 bitTypography="h5">{{ "paymentMethod" | i18n }}</h5>
<app-enter-payment-method
[showBankAccount]="true"
[showAccountCredit]="false"
[group]="formGroup.controls.paymentForm"
[includeBillingAddress]="false"
#paymentComponent
></app-enter-payment-method>
<app-display-payment-method
[subscriber]="subscriber()"
[paymentMethod]="paymentMethod()"
[hideHeader]="true"
>
</app-display-payment-method>
<h5 bitTypography="h5" class="tw-pt-4 tw-pb-2">{{ "billingAddress" | i18n }}</h5>
<app-enter-billing-address
[group]="formGroup.controls.billingAddress"
@@ -40,7 +39,7 @@
bitButton
bitFormButton
buttonType="primary"
[disabled]="loading() || !isFormValid()"
[disabled]="loading() || !formGroup.valid"
type="submit"
>
{{ "upgrade" | i18n }}

View File

@@ -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<void>();
protected complete = output<PremiumOrgUpgradePaymentResult>();
readonly paymentComponent = viewChild.required(EnterPaymentMethodComponent);
readonly cartSummaryComponent = viewChild.required(CartSummaryComponent);
protected formGroup = new FormGroup({
organizationName: new FormControl<string>("", [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<MaskedPaymentMethod | null>(null);
protected readonly subscriber = signal<BitwardenSubscriber | null>(null);
protected readonly planMembershipMessage = computed<string>(
() => 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<Cart>(() => {
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<void> {
// 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<void> => {
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<PremiumOrgUpgradePaymentResult> {
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,