richnologies / ngx-stripe

Angular 6+ wrapper for StripeJS
MIT License
217 stars 77 forks source link

[Feature Request] - Support for Changing Layouts in the Payment Element #213

Closed imuchene closed 10 months ago

imuchene commented 1 year ago

When using the payment element without a custom payment form, the default layout rendered is the tabs layout. I'd kindly request that support be added for different layouts such as the accordion layout. When I try to modify the layout currently I get the following error in the browser console:

ERROR IntegrationError: Can only create one Element of type payment.
    at (index):1:296293
    at t.<anonymous> ((index):1:296994)
    at t.create ((index):1:92333)
    at StripeDialogComponent.ngAfterViewChecked (stripe-dialog.component.ts:57:41)
    at callHook (core.mjs:2497:18)
    at callHooks (core.mjs:2456:17)
    at executeCheckHooks (core.mjs:2388:5)
    at refreshView (core.mjs:10431:21)
    at refreshEmbeddedViews (core.mjs:11381:17)
    at refreshView (core.mjs:10390:9)

My payment element component:


import { AfterViewChecked, Component, Inject, OnInit, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { StripeElementsOptions, StripePaymentElementOptions } from '@stripe/stripe-js';
import { StripePaymentElementComponent, StripeService } from 'ngx-stripe';
import { Payment } from './payment';

@Component({
  selector: 'app-stripe-dialog',
  templateUrl: './stripe-dialog.component.html',
  styleUrls: ['./stripe-dialog.component.scss']
})
export class StripeDialogComponent implements OnInit, AfterViewChecked {
  @ViewChild(StripePaymentElementComponent)
  stripePaymentElement: StripePaymentElementComponent;

  elementsOptions: StripeElementsOptions = {
    locale: 'en-GB',
    appearance: {
      theme: 'flat',
    },
  };

  paymentElementOptions : StripePaymentElementOptions = {
    layout: {
      type:'accordion',
      defaultCollapsed: false,
      radios: true,
      spacedAccordionItems: false
    }
  }

  paying: boolean = false;
  paymentData: Payment;
  clientSecret: string;

  constructor(
    @Inject(MAT_DIALOG_DATA)
    data: any,
    private stripeService: StripeService
  ) {
    this.paymentData = data.data;

    if (this.paymentData) {
      this.clientSecret = this.paymentData.clientSecret;
    }

  }

  ngOnInit(): void {
    this.elementsOptions.clientSecret = this.clientSecret;
  }

  ngAfterViewChecked(): void {
    this.stripePaymentElement.elements?.create('payment', this.paymentElementOptions);
  }

  pay() {
    this.paying = true;
    this.stripeService
      .confirmPayment({
        elements: this.stripePaymentElement.elements,
        confirmParams: {
          return_url: 'https://localhost:4200/payment',
          payment_method_data: {
            billing_details: {
              name: this.paymentData.name
            }
          }
        },
        redirect: 'if_required'
      })
      .subscribe(
        {
          next: (result) => {
            this.paying = false;
            console.log('payment result', result);

            if (result.error) {
              // Show the error to the customer e.g. insufficient funds
              alert(result.error.message);
            } else {
              if (result.paymentIntent?.status === 'succeeded') {
                // Show a success message to your customer
                alert('Payment was successful');
              }
            }
          },
          error: (error) => {
            console.error('An error occurred when completing the payment', error);
          }
        }
      );
  }
}

My payment element component:

<h2 mat-dialog-title>Finalize Payment</h2>
<mat-dialog-content class="mat-typography">
  <h3>Add Payment Details Below:</h3>
  <br/>
  <ng-container *ngIf="elementsOptions?.clientSecret as clientSecret">
    <ngx-stripe-payment
      [elementsOptions]="elementsOptions"
      [clientSecret]="clientSecret"
    ></ngx-stripe-payment>
  </ng-container>

</mat-dialog-content>
  <mat-dialog-actions align="center">
    <button type="submit" mat-raised-button color="primary" (click)="pay()">
      Pay $10
    </button>
    <button mat-button [mat-dialog-close]="true">Close</button>
  </mat-dialog-actions>  

The payment class:

export class Payment {
  amount: number;
  paymentMethodId: string;
  name: string;
  clientSecret: string;
}

I'm using angular material as my responsive CSS framework.

richnologies commented 1 year ago

Hey @imuchene, thanks for reaching out with such a detail explanation. It helps a lot.

You can already do what you ask. Here is an example where I have a Payment element with an accordion layout:

https://stackblitz.com/edit/ngx-stripe-issue-213?file=src%2Fapp%2Fpluto.service.ts,src%2Fapp%2Fapp.component.ts,src%2Fapp%2Fapp.module.ts&file=src%2Fapp%2Fapp.component.html

The layout is part of the element options. You can pass it as parameter:

<ngx-stripe-payment
  [options]="options"
  [appearance]="appearance"
  [clientSecret]="elementsOptions?.clientSecret"
></ngx-stripe-payment>
options: StripePaymentElementOptions = {
  layout: {
    type: 'accordion',
  },
};

You a lot of options to fine tune the look and feel. Check the documentation: https://stripe.com/docs/payments/customize-payment-element#layouts

As for the error you're getting, I might be wrong, but this block of code does not look good to me. You shouldn't have to create the element yourself. The library does it for you and is capable to adapt to any changes in your options for example. Here is the component source code: https://github.com/richnologies/ngx-stripe/blob/main/projects/ngx-stripe/src/lib/components/payment-element.component.ts

What I think is happening here is that the library is creating one payment element and you're trying to create a second one and that is not allowed on the same elements object. (or maybe you create the first one and then the library tries to create the second, it doesn't matter)

ngAfterViewChecked(): void {
  this.stripePaymentElement.elements?.create('payment', this.paymentElementOptions);
}

Let me know if this helps

Kind regards

R

imuchene commented 1 year ago

Hello @richnologies ,

Thanks for the quick response. I was able to adapt the Stackblitz sample you provided, and to successfully change the layout of the payment elements as shown in the screenshot below:

PaymentElement with Accordion

You're right that I was doing something wrong by re-creating the payment element a second time. This was my fault, as I was trying various things to make the layout change, but couldn't quite figure it out. The only thing pending would be to add a note to the documentation about the options parameter in the ngx-stripe-payment component. I would gladly assist in the task, if it hasn't been done already.

richnologies commented 1 year ago

Hey @imuchene, by all means. Here is the docs page link: https://github.com/richnologies/ngx-stripe/blob/main/projects/ngx-stripe-docs/src/app/docs/payment-element/payment-element.component.ts

The format is a bit rudimentary, but on the bright side is easy to understand. Feel free to open a PR with what would have help you. We can review it and merge it.

Really appreciate the help

Regards

R