import { ChangeDetectionStrategy, Component, computed, inject, input, output, signal, viewChild, } from "@angular/core"; import { FormGroup } from "@angular/forms"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ToastService, IconComponent } from "@bitwarden/components"; import { LogService } from "@bitwarden/logging"; import { SubscriberBillingClient } from "@bitwarden/web-vault/app/billing/clients"; import { SharedModule } from "../../../shared"; import { BitwardenSubscriber } from "../../types"; import { getCardBrandIcon, MaskedPaymentMethod, TokenizablePaymentMethods } from "../types"; import { EnterPaymentMethodComponent } from "./enter-payment-method.component"; /** * Component for inline editing of payment methods. * Displays a form to update payment method details directly within the parent view. */ @Component({ selector: "app-display-payment-method-inline", changeDetection: ChangeDetectionStrategy.OnPush, template: ` @if (!isChangingPayment()) {
{{ "paymentMethod" | i18n }}
@if (paymentMethod(); as pm) { @switch (pm.type) { @case ("bankAccount") { @if (pm.hostedVerificationUrl) {

{{ "verifyBankAccountWithStripe" | i18n }} {{ "verifyNow" | i18n }}

}

{{ pm.bankName }}, *{{ pm.last4 }} @if (pm.hostedVerificationUrl) { - {{ "unverified" | i18n }} }

} @case ("card") {

@if (cardBrandIcon(); as icon) { } @else { } {{ pm.brand | titlecase }}, *{{ pm.last4 }}, {{ pm.expiration }}

} @case ("payPal") {

{{ pm.email }}

} } } @else {

{{ "noPaymentMethod" | i18n }}

} @let key = paymentMethod() ? "changePaymentMethod" : "addPaymentMethod"; {{ key | i18n }}
} @else { @if (showFormButtons()) {
} }
`, standalone: true, imports: [SharedModule, EnterPaymentMethodComponent, IconComponent], providers: [SubscriberBillingClient], }) export class DisplayPaymentMethodInlineComponent { readonly subscriber = input.required(); readonly paymentMethod = input.required(); readonly externalFormGroup = input(null); readonly updated = output(); protected formGroup: FormGroup; private readonly enterPaymentMethodComponent = viewChild( EnterPaymentMethodComponent, ); readonly isChangingPayment = signal(false); protected readonly cardBrandIcon = computed(() => getCardBrandIcon(this.paymentMethod())); // Show submit buttons only when component is managing its own form (no external form provided) protected readonly showFormButtons = computed(() => this.externalFormGroup() === null); private readonly billingClient = inject(SubscriberBillingClient); private readonly i18nService = inject(I18nService); private readonly toastService = inject(ToastService); private readonly logService = inject(LogService); constructor() { // Use external form group if provided, otherwise create our own this.formGroup = this.externalFormGroup() ?? EnterPaymentMethodComponent.getFormGroup(); } /** * Initiates the payment method change process by displaying the inline form. */ protected changePaymentMethod = async (): Promise => { this.isChangingPayment.set(true); }; /** * Public method to get tokenized payment method data. * Use this when parent component handles submission. * Parent is responsible for handling billing address separately. * @returns Promise with tokenized payment method */ async getTokenizedPaymentMethod(): Promise { if (!this.formGroup.valid) { this.formGroup.markAllAsTouched(); throw new Error("Form is invalid"); } const component = this.enterPaymentMethodComponent(); if (!component) { throw new Error("Payment method component not found"); } const paymentMethod = await component.tokenize(); if (!paymentMethod) { throw new Error("Failed to tokenize payment method"); } return paymentMethod; } /** * Validates the form and returns whether it's ready for submission. * Used when parent component handles submission to determine button state. */ isFormValid(): boolean { const enterPaymentMethodComponent = this.enterPaymentMethodComponent(); if (enterPaymentMethodComponent) { return this.enterPaymentMethodComponent()!.validate(); } return false; } /** * Public method to reset the form and exit edit mode. * Use this after parent successfully handles the update. */ resetForm(): void { this.formGroup.reset(); this.isChangingPayment.set(false); } /** * Submits the payment method update form. * Validates the form, tokenizes the payment method, and sends the update request. */ protected submit = async (): Promise => { try { const paymentMethod = await this.getTokenizedPaymentMethod(); const billingAddress = this.formGroup.value.type !== TokenizablePaymentMethods.payPal ? this.formGroup.controls.billingAddress.getRawValue() : null; await this.handlePaymentMethodUpdate(paymentMethod, billingAddress); } catch (error) { this.logService.error("Error submitting payment method update:", error); this.toastService.showToast({ variant: "error", title: "", message: this.i18nService.t("paymentMethodUpdateError"), }); throw error; } }; /** * Handles the payment method update API call and result processing. */ private async handlePaymentMethodUpdate(paymentMethod: any, billingAddress: any): Promise { const result = await this.billingClient.updatePaymentMethod( this.subscriber(), paymentMethod, billingAddress, ); switch (result.type) { case "success": { this.toastService.showToast({ variant: "success", title: "", message: this.i18nService.t("paymentMethodUpdated"), }); this.updated.emit(result.value); this.resetForm(); break; } case "error": { this.logService.error("Error submitting payment method update:", result); this.toastService.showToast({ variant: "error", title: "", message: this.i18nService.t("paymentMethodUpdateError"), }); break; } } } /** * Cancels the inline editing and resets the form. */ protected cancel = (): void => { this.resetForm(); }; }