import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; import { Subject } from "rxjs"; import { takeUntil } from "rxjs/operators"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-source.request"; import { SharedModule } from "../../../shared"; import { BillingServicesModule, BraintreeService, StripeService } from "../../services"; import { PaymentLabelV2 } from "./payment-label-v2.component"; /** * Render a form that allows the user to enter their payment method, tokenize it against one of our payment providers and, * optionally, submit it using the {@link onSubmit} function if it is provided. * * This component is meant to replace the existing {@link PaymentComponent} which is using the deprecated Stripe Sources API. */ @Component({ selector: "app-payment-v2", templateUrl: "./payment-v2.component.html", standalone: true, imports: [BillingServicesModule, SharedModule, PaymentLabelV2], }) export class PaymentV2Component implements OnInit, OnDestroy { /** Show account credit as a payment option. */ @Input() showAccountCredit: boolean = true; /** Show bank account as a payment option. */ @Input() showBankAccount: boolean = true; /** Show PayPal as a payment option. */ @Input() showPayPal: boolean = true; /** The payment method selected by default when the component renders. */ @Input() private initialPaymentMethod: PaymentMethodType = PaymentMethodType.Card; /** If provided, will be invoked with the tokenized payment source during form submission. */ @Input() protected onSubmit?: (request: TokenizedPaymentSourceRequest) => Promise; @Output() submitted = new EventEmitter(); private destroy$ = new Subject(); protected formGroup = new FormGroup({ paymentMethod: new FormControl(null), bankInformation: new FormGroup({ routingNumber: new FormControl("", [Validators.required]), accountNumber: new FormControl("", [Validators.required]), accountHolderName: new FormControl("", [Validators.required]), accountHolderType: new FormControl("", [Validators.required]), }), }); protected PaymentMethodType = PaymentMethodType; constructor( private billingApiService: BillingApiServiceAbstraction, private braintreeService: BraintreeService, private stripeService: StripeService, ) {} ngOnInit(): void { this.formGroup.controls.paymentMethod.patchValue(this.initialPaymentMethod); this.stripeService.loadStripe( { cardNumber: "#stripe-card-number", cardExpiry: "#stripe-card-expiry", cardCvc: "#stripe-card-cvc", }, this.initialPaymentMethod === PaymentMethodType.Card, ); if (this.showPayPal) { this.braintreeService.loadBraintree( "#braintree-container", this.initialPaymentMethod === PaymentMethodType.PayPal, ); } this.formGroup .get("paymentMethod") .valueChanges.pipe(takeUntil(this.destroy$)) .subscribe((type) => { this.onPaymentMethodChange(type); }); } /** Programmatically select the provided payment method. */ select = (paymentMethod: PaymentMethodType) => { this.formGroup.get("paymentMethod").patchValue(paymentMethod); }; protected submit = async () => { const { type, token } = await this.tokenize(); await this.onSubmit({ type, token }); this.submitted.emit(type); }; /** * Tokenize the payment method information entered by the user against one of our payment providers. * * - {@link PaymentMethodType.Card} => [Stripe.confirmCardSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_card_setup} * - {@link PaymentMethodType.BankAccount} => [Stripe.confirmUsBankAccountSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_us_bank_account_setup} * - {@link PaymentMethodType.PayPal} => [Braintree.requestPaymentMethod]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html#requestPaymentMethod} * */ async tokenize(): Promise<{ type: PaymentMethodType; token: string }> { const type = this.selected; if (this.usingStripe) { const clientSecret = await this.billingApiService.createSetupIntent(type); if (this.usingBankAccount) { const token = await this.stripeService.setupBankAccountPaymentMethod(clientSecret, { accountHolderName: this.formGroup.value.bankInformation.accountHolderName, routingNumber: this.formGroup.value.bankInformation.routingNumber, accountNumber: this.formGroup.value.bankInformation.accountNumber, accountHolderType: this.formGroup.value.bankInformation.accountHolderType, }); return { type, token, }; } if (this.usingCard) { const token = await this.stripeService.setupCardPaymentMethod(clientSecret); return { type, token, }; } } if (this.usingPayPal) { const token = await this.braintreeService.requestPaymentMethod(); return { type, token, }; } return null; } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); this.stripeService.unloadStripe(); if (this.showPayPal) { this.braintreeService.unloadBraintree(); } } private onPaymentMethodChange(type: PaymentMethodType): void { switch (type) { case PaymentMethodType.Card: { this.stripeService.mountElements(); break; } case PaymentMethodType.PayPal: { this.braintreeService.createDropin(); break; } } } get selected(): PaymentMethodType { return this.formGroup.value.paymentMethod; } protected get usingAccountCredit(): boolean { return this.selected === PaymentMethodType.Credit; } protected get usingBankAccount(): boolean { return this.selected === PaymentMethodType.BankAccount; } protected get usingCard(): boolean { return this.selected === PaymentMethodType.Card; } protected get usingPayPal(): boolean { return this.selected === PaymentMethodType.PayPal; } private get usingStripe(): boolean { return this.usingBankAccount || this.usingCard; } }