Stripe Elemnts Stimulus Controller


#1

Hi All! Before I try and spend the time figuring it out, I figured I would ask if anyone had built a Stimulus controller to handle Stripe that they wouldn’t mind me looking at?

Otherwise, I will start building one out! I am just caught up on not putting too many things into connect() :thinking:

This is my current js file handling it, but due to some turbolinks issues, I am looking to convert it to stimulus.

// PSJS = Page Specific Javascript
document.addEventListener("turbolinks:load", function () {

  if (!isStripeController()) {
    return
    console.log('didn\'t pass isStripeController test')
  }

  function isStripeController() {
    var meta_data = document.querySelector("meta[name='psjs']").dataset
    var controller_name = meta_data.controller
    var action_name = meta_data.action
    if ((controller_name == 'subscriptions' || controller_name == 'cards') && (action_name == 'edit' || action_name == 'new')) {
      return true
    }
  }
  console.log('isStripeController must have passed here!')

  var public_key = document.querySelector("meta[name='stripe-public-key']").content
  var stripe = Stripe(public_key)
  var elements = stripe.elements();
  var use_diff_card = document.querySelector('.use-different-card');

  // Custom styling can be passed to options when creating an Element.
  var style = {
    base: {
      // Add your base input styles here. For example:
      fontSize: '16px',
      color: "#32325d",
    }
  };

  // Create an instance of the card Element.
  var card = elements.create('card', {
    style: style
  });

  // Add an instance of the card Element into the `card-element` <div>.
  card.mount('#card-element');



  card.addEventListener('change', function (event) {
    var displayError = document.getElementById('card-errors');
    if (event.error) {
      displayError.textContent = event.error.message;
    } else {
      displayError.textContent = '';
    }
  });

  // Create a token or display an error when the form is submitted.
  var form = document.getElementById('payment-form');
  form.addEventListener('submit', function (event) {
    event.preventDefault();
    stripe.createToken(card).then(function (result) {
      if (result.error) {
        // Inform the customer that there was an error.
        var errorElement = document.getElementById('card-errors');
        errorElement.textContent = result.error.message;
        event.preventDefault();
        console.log('Card result.error was called')
        // return false;
      } else {
        // Send the token to your server.
        console.log('there wasn\'t errors under result.error, \
        so hopefully stripeTokenHandler was called? \
        Anyway, here is the result.token: ' + result.token)
        stripeTokenHandler(result.token);
      }
    });
  });



  function stripeTokenHandler(token) {
    // Insert the token ID into the form so it gets submitted to the server
    console.log('stripeTokenHandler was called here.\
   Here is the token handed to this stripeTokenHandler: ' + token.content)
    var form = document.getElementById('payment-form');
    var hiddenInput = document.createElement('input');
    hiddenInput.setAttribute('type', 'hidden');
    hiddenInput.setAttribute('name', 'stripeToken');
    hiddenInput.setAttribute('value', token.id);
    form.appendChild(hiddenInput);

    ["brand", "exp_month", "exp_year", "last4"].forEach(function (field) {
      addFieldToForm(form, token, field);
    });

    // Submit the form
    form.submit();
    console.log('If this was called, the form.submit was also called...')

  }

  function addFieldToForm(form, token, field) {
    var hiddenInput = document.createElement('input');
    hiddenInput.setAttribute('type', 'hidden');
    hiddenInput.setAttribute('name', "stripe_card_" + field);
    hiddenInput.setAttribute('value', token.card[field]);
    form.appendChild(hiddenInput);
  }
});

#2

I’ve implemented a controller for handling credit card details. The controller is below, using TypeScript:

import Stripe = stripe;
import { Controller } from 'stimulus';
import * as $ from 'jquery';

export default class extends Controller {
  static targets = [
    'token',
    'cardElement',
    'cardErrors',
    'cardholder',
    'addressLine1',
    'addressLine2',
    'addressCity',
    'addressState',
    'addressZip',
    'submitButton'
  ];

  private tokenTarget: HTMLInputElement;
  private cardElementTarget: HTMLInputElement;
  private cardErrorsTarget: HTMLInputElement;
  private cardholderTarget: HTMLInputElement;
  private addressLine1Target: HTMLInputElement;
  private addressLine2Target: HTMLInputElement;
  private addressCityTarget: HTMLInputElement;
  private addressStateTarget: HTMLInputElement;
  private addressZipTarget: HTMLInputElement;
  private submitButtonTarget: HTMLButtonElement;

  private elements: Stripe.elements.Elements;
  private stripe: Stripe.Stripe;
  private cardElement: Stripe.elements.Element;

  private submitButtonText: string;

  connect() {
    this.setupForm();
    this.submitButtonText = this.submitButtonTarget.innerText;
  }

  setupForm() {
    let stripeKey = document.querySelector('meta[name="stripe"]').getAttribute('content');

    this.stripe = Stripe(stripeKey);

    this.elements = this.stripe.elements();

    this.cardElement = this.elements.create('card', {
      hidePostalCode: true,
      classes: {
        focus: 'has-focus'
      },
      style: this.baseStyle
    });

    this.cardElement.mount(this.cardElementTarget);

    this.cardElement.on('change', this.handleChange);

    $(this.element).on('submit', this.handleSubmit);
  }

  handleChange = (event: Stripe.elements.ElementChangeResponse) => {
    if (event.error) {
      this.displayError(event.error.message);
    } else {
      this.displayError('');
    }
  };

  displayError(message: string, updateElement = false) {
    this.cardErrorsTarget.textContent = message;

    if (message) {
      this.cardErrorsTarget.parentElement.classList.add('is-invalid');
    } else {
      this.cardErrorsTarget.parentElement.classList.remove('is-invalid');
    }

    if (updateElement) {
      this.cardElementTarget.classList.add('StripeElement--invalid');
    }
  }

  handleSubmit = (event: JQuery.Event) => {
    event.preventDefault();

    this.disableButton();

    this.stripe.createToken(this.cardElement, this.cardDetails)
      .then(this.stripeResponseHandler);

    return false;
  };

  disableButton() {
    $(this.submitButtonTarget)
      .prop('disabled', true)
      .text('Submitting...');
  }

  enableButton() {
    $(this.submitButtonTarget)
      .prop('disabled', false)
      .text(this.submitButtonText);
  }

  stripeResponseHandler = (token: Stripe.TokenResponse) => {
    this.tokenTarget.value = token.token.id;

    $.ajax({
      type: 'POST',
      url: this.formAction,
      data: $(this.element).serialize()
    })
      .then((data) => {
        
      })
      .catch((data: JQuery.jqXHR, textStatus: string, errorThrown: string) => {
        this.displayError(data.responseText, true);
        this.enableButton();
      });
  };

  get baseStyle() {
    return {
      base: {
        color: '#32325d',
        fontSmoothing: 'antialiased',
        fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
        fontSize: '14px',
        '::placeholder': {
          color: '#aab7c4'
        }
      },
      invalid: {
        color: '#dc3545',
        iconColor: '#dc3545'
      }
    }
  }

  get cardDetails(): Stripe.TokenOptions {
    return {
      name: this.cardholderTarget.value,
      address_line1: this.addressLine1Target.value,
      address_line2: this.addressLine2Target.value,
      address_city: this.addressCityTarget.value,
      address_state: this.addressStateTarget.value,
      address_zip: this.addressZipTarget.value,
      address_country: 'US'
    };
  }

  get formAction() {
    return (<HTMLFormElement>this.element).action;
  }
}