Home Reference Source

src/contrib/stripe/stripe/Stripe.js

/* globals Stripe */
import _ from 'lodash';
import Validator from '../../../validator/Validator';
import Component from '../../../components/_classes/component/Component';
import { GlobalFormio as Formio } from '../../../Formio';
import NativePromise from 'native-promise-only';

// Register a custom validor to use card validition from Stripe
if (typeof Validator.validators.stripe === 'undefined') {
  Validator.validators.stripe =  {
    key: 'validate.stripe',
    message(component) {
      let stripeMessage = '';
      if (component.lastResult && component.lastResult.error) {
        stripeMessage = component.lastResult.error.message;
      }
      return component.t(component.errorMessage('stripe'), {
        field: component.errorLabel,
        stripe: stripeMessage,
        stripeError: component.lastResult.error,
        data: component.data
      });
    },
    check(component, setting, value) {
      if (!component.paymentDone && component.lastResult) {
        return !component.lastResult.error && !component.isEmpty(value);
      }
      return true;
    }
  };
}

/**
 * This is the StripeComponent class.
 */
export default class StripeComponent extends Component {
  constructor(component, options, data) {
    super(component, options, data);

    // Get the source for Stripe API
    const src = 'https://js.stripe.com/v3/';

    /**
     * Promise when Stripe is ready.
     * @type {Promise}
     */
    this.stripeReady = Formio.requireLibrary('stripe', 'Stripe', src, true);

    /**
     * The last result returned by Stripe.
     * @type {Object}
     */
    this.lastResult = null;

    /**
     * The state of the payment.
     * @type {Boolean}
     */
    this.paymentDone = false;

    // Use stripe validator
    this.validators.push('stripe');
  }

  elementInfo() {
    const info = super.elementInfo();
    info.type = 'input';
    info.attr.type = 'hidden';
    info.changeEvent = 'change';
    return info;
  }

  /**
   * Set CSS classes for pending authorization
   */
  authorizePending() {
    this.addClass(this.element, 'stripe-submitting');
    this.removeClass(this.element, 'stripe-error');
    this.removeClass(this.element, 'stripe-submitted');
  }

  /**
   * Set CSS classes and display error when error occurs during authorization
   * @param {Object} resultError - The result error returned by Stripe API.
   */
  authorizeError(resultError) {
    this.removeClass(this.element, 'stripe-submitting');
    this.addClass(this.element, 'stripe-submit-error');
    this.removeClass(this.element, 'stripe-submitted');

    if (!this.lastResult) {
      this.lastResult = {};
    }
    this.lastResult.error = resultError;
    this.setValue(this.getValue(), {
      changed: true
    });
  }

  /**
   * Set CSS classes and save token when authorization successed
   * @param {Object} result - The result returned by Stripe API.
   */
  authorizeDone(result) {
    this.removeClass(this.element, 'stripe-submit-error');
    this.removeClass(this.element, 'stripe-submitting');
    this.addClass(this.element, 'stripe-submitted');

    this.stripeSuccess.style.display = 'block';
    if (this.component.stripe.payButton && this.component.stripe.payButton.enable) {
      this.stripeElementPayButton.style.display = 'none';
      this.stripeSeparator.style.display = 'none';
    }
    this.stripeElementCard.style.display = 'none';

    // Store token in hidden input
    this.setValue(result.token.id);

    this.paymentDone = true;
  }

  /**
   * Call Stripe API to get token
   */
  authorize() {
    if (this.paymentDone) {
      return;
    }

    const that = this;
    return new NativePromise(((resolve, reject) => {
      that.authorizePending();

      // Get all additionnal data to send to Stripe
      const cardData = _.cloneDeep(that.component.stripe.cardData) || {};
      _.each(cardData, (value, key) => {
        cardData[key] = that.t(value);
      });

      return that.stripe.createToken(that.stripeCard, cardData).then((result) => {
        if (result.error) {
          that.authorizeError(result.error);
          reject(result.error);
        }
        else {
          that.authorizeDone(result);
          resolve();
        }
      });
    }));
  }

  /**
   * Handle event dispatched by Stripe library
   * @param {Object} result - The result returned by Stripe.
   */
  onElementCardChange(result) {
    // If the field is not required and the field is empty, do not throw an error
    if (result.empty && (!this.component.validate || !this.component.validate.required)) {
      delete result.error;
    }

    // Force change when complete or when an error is thrown or fixed
    const changed = result.complete
      || this.lastResult && (!!this.lastResult.error !== !!result.error)
      || this.lastResult && this.lastResult.error && result.error && this.lastResult.error.code !== result.error.code
      || false;
    this.lastResult = result;

    // When the field is not empty, use "." as value to not trigger "required" validator
    const value = result.empty ? '' : '.';
    this.setValue(value, {
      changed: changed
    });
  }

  beforeSubmit() {
    // Get the token before submitting when the field is not empty or required
    if (this.lastResult && !this.lastResult.empty || (this.component.validate && this.component.validate.required)) {
      return this.authorize();
    }
  }

  build() {
    super.build();

    const successLabel = this.component.stripe.payButton.successLabel || 'Payment successful';
    this.stripeSuccess = this.ce('div', {
      class: 'Stripe-success',
      style: 'display: none'
    }, this.t(successLabel));
    this.element.appendChild(this.stripeSuccess);

    // Add container for pay button
    if (this.component.stripe.payButton && this.component.stripe.payButton.enable) {
      this.stripeElementPayButton = this.ce('div', {
        class: 'Stripe-paybutton'
      });
      this.element.appendChild(this.stripeElementPayButton);

      const separatorLabel = this.component.stripe.payButton.separatorLabel || 'Or';
      this.stripeSeparator = this.ce('div', {
        class: 'Stripe-separator',
        style: 'display: none'
      }, this.t(separatorLabel));
      this.element.appendChild(this.stripeSeparator);
    }

    // Create container for stripe cart input
    this.stripeElementCard = this.ce('div');
    this.element.appendChild(this.stripeElementCard);

    this.stripeReady.then(() => {
      this.stripe = new Stripe(this.component.stripe.apiKey);

      // Create an instance of Elements
      let stripeElementsOptions = {};
      if (this.component.stripe) {
        stripeElementsOptions = _.cloneDeep(this.component.stripe.stripeElementsOptions) || {};
      }
      if (typeof stripeElementsOptions.locale === 'undefined') {
        stripeElementsOptions.locale = this.options.language;
      }
      const elements = this.stripe.elements(stripeElementsOptions);

      // Create an instance of the card Element
      let stripeElementOptions = {};
      if (this.component.stripe) {
        stripeElementOptions = this.component.stripe.stripeElementOptions || {};
      }
      this.stripeCard = elements.create('card', stripeElementOptions);

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

      // Handle real-time validation errors from the card Element.
      this.addEventListener(this.stripeCard, 'change', this.onElementCardChange.bind(this));

      // If there is a pay button, then create it and add listener
      if (this.component.stripe.payButton && this.component.stripe.payButton.enable) {
        const paymentRequest = this.stripe.paymentRequest(this.component.stripe.payButton.paymentRequest);

        this.addEventListener(paymentRequest, 'token', (result) => {
          this.authorizeDone(result, true);
          result.complete('success');
        });

        let stripeOptionsPayButton = {};
        if (this.component.stripe.payButton) {
          stripeOptionsPayButton = this.component.stripe.payButton.stripeOptions || {};
        }
        stripeOptionsPayButton.paymentRequest = paymentRequest;

        const paymentRequestElement = elements.create('paymentRequestButton', stripeOptionsPayButton);

        paymentRequest.canMakePayment().then((result) => {
          if (result) {
            // Display label separator
            this.stripeSeparator.style.display = 'block';
            paymentRequestElement.mount(this.stripeElementPayButton);
          }
        });
      }
    });
  }
}

if (typeof global === 'object' && global.Formio && global.Formio.registerComponent) {
  global.Formio.registerComponent('stripe', StripeComponent);
}