import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Inject, Input, OnDestroy, Output, ViewChild, } from '@angular/core'; import { STRIPE_ERROR_CODES, StripeChangeEventInterface, StripeInterface, StripePaymentVersion, StripeSingleElementInterface, StripeStyleInterface, } from './../../models/index'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'stripe-elements-component', styleUrls: [ './stripe-elements.component.scss', ], templateUrl: './stripe-elements.component.template.pug', }) export class StripeElementsComponent implements AfterViewInit, OnDestroy { @Input() public cardNumberPlaceholder = ''; @Input() public cardExpiryPlaceholder = ''; @Input() public cardCvcPlaceholder = ''; @Input() public inputStyling: StripeStyleInterface = { classes: { base: 'c-stripe-elements__stripe-element', empty: 'c-stripe-elements__stripe-element--empty', focus: 'c-stripe-elements__stripe-element--focus', invalid: 'c-stripe-elements__stripe-element--invalid', }, placeholder: '', style: { base: { '::placeholder': { color: '#aab7c4', }, 'color': '#32325d', 'fontFamily': 'sans-serif', 'fontSize': '16px', 'fontSmoothing': 'antialiased', 'fontWeight': 500, }, invalid: { color: '#32325d', }, }, }; @Input() public submitButtonClasses: string[] | string; @Input() public submitButtonWrapperClasses: string[] | string; @Input() public submitButtonLabel = 'Save'; @Input() public isLoadingExternalData = false; @Input() public buttonType = 'button'; @Input() public apiKey: string; @Input() public totalPrice: number; @Input() public currencyToUse: string; @Input() public version: StripePaymentVersion = StripePaymentVersion.V2; @Input() public paymentIntentClientSecret: string; @Output() public onSubmitClick = new EventEmitter(); @Output() public onSuccess = new EventEmitter(); @Output() public onCardDeclined = new EventEmitter(); @Output() public onCvcInfoButtonPressed = new EventEmitter(); public stripeCardNumber: StripeSingleElementInterface; public stripeCardExpiry: StripeSingleElementInterface; public stripeCardCvc: StripeSingleElementInterface; public cardNumberError: string; public cardExpiryError: string; public cardCvcError: string; public isLoadingStripeData = false; @ViewChild('cardNumberElement') public cardNumberElement: ElementRef; @ViewChild('cardExpiryElement') public cardExpiryElement: ElementRef; @ViewChild('cardCvcElement') public cardCvcElement: ElementRef; public get isCvcButtonVisible() { return this.onCvcInfoButtonPressed.observers.length > 0; } public get isButtonLoading() { return this.isLoadingExternalData || this.isLoadingStripeData; } constructor( @Inject('Stripe') private _stripe: StripeInterface, private _changeDetectorRef: ChangeDetectorRef, ) {} public ngAfterViewInit() { const elements = this._stripe.elements(); this.stripeCardNumber = elements.create('cardNumber', this.inputStyling); this.stripeCardNumber.mount(this.cardNumberElement.nativeElement); this.stripeCardNumber.addEventListener('change', (value) => ( this.updateCardNumberError(value) )); this.stripeCardExpiry = elements.create('cardExpiry', this.inputStyling); this.stripeCardExpiry.mount(this.cardExpiryElement.nativeElement); this.stripeCardExpiry.addEventListener('change', (value) => ( this.updateCardExpiryError(value) )); this.stripeCardCvc = elements.create('cardCvc', this.inputStyling); this.stripeCardCvc.mount(this.cardCvcElement.nativeElement); this.stripeCardCvc.addEventListener('change', (value) => ( this.updateCardCvcError(value) )); } public ngOnDestroy() { [ this.stripeCardNumber, this.stripeCardExpiry, this.stripeCardCvc, ].forEach((item) => { item.removeEventListener('change'); item.destroy(); }); } public updateCardNumberError({ error }: StripeChangeEventInterface) { this.cardNumberError = error && error.message; this._changeDetectorRef.markForCheck(); } public updateCardExpiryError({ error }: StripeChangeEventInterface) { this.cardExpiryError = error && error.message; this._changeDetectorRef.markForCheck(); } public updateCardCvcError({ error }: StripeChangeEventInterface) { this.cardCvcError = error && error.message; this._changeDetectorRef.markForCheck(); } public updatePaymentStatus(state: boolean) { this.isLoadingStripeData = state; this._changeDetectorRef.markForCheck(); } public onPaymentError(err: { message?: string }) { // tslint:disable-next-line console.error(err); this.onCardDeclined.emit(err.message || 'An error occurred.'); this.updatePaymentStatus(false); } public processStripePaymentV1() { this.isLoadingStripeData = true; this._changeDetectorRef.markForCheck(); return this._stripe.createToken(this.stripeCardNumber) .then((result) => { const { error, token } = result; if (token) { this.onSuccess.emit(token.id); } else if ( error && error.code === STRIPE_ERROR_CODES.CARD_DECLINED ) { this.onCardDeclined.emit(error.message); } this.isLoadingStripeData = false; this._changeDetectorRef.markForCheck(); }); } public async confirmCardPaymentV2() { try { const stripePaymentResponse = await this._stripe.confirmCardPayment(this.paymentIntentClientSecret, { payment_method: { card: this.stripeCardNumber, }, }); if (stripePaymentResponse.error) { this.onPaymentError(stripePaymentResponse.error); return; } this.onSuccess.emit(); } catch (error) { this.onPaymentError(error); } } public processStripePaymentV2() { this.updatePaymentStatus(true); if (!this.paymentIntentClientSecret) { this.onPaymentError({ message: 'Error code - PI.' }); return; } this.confirmCardPaymentV2(); } public onSubmit() { if (this.isButtonLoading) { return; } this.onSubmitClick.emit(); switch (this.version) { case StripePaymentVersion.V1: this.processStripePaymentV1(); break; case StripePaymentVersion.V2: this.processStripePaymentV2(); break; default: throw new Error(`StripePaymentVersion ${this.version} is invalid.`); } } public cvcInfoButtonPressed() { this.onCvcInfoButtonPressed.emit(); } }