paypal/external/paypal.js

'use strict';

var frameService = require('../../frame-service/external');
var BraintreeError = require('../../lib/error');
var VERSION = require('package.version');
var constants = require('../shared/constants');
var INTEGRATION_TIMEOUT_MS = require('../../lib/constants').INTEGRATION_TIMEOUT_MS;
var analytics = require('../../lib/analytics');

/**
 * @typedef {object} PayPal~tokenizePayload
 * @property {string} nonce The payment method nonce
 * @property {string} type Always <code>PayPalAccount</code>
 * @property {object} details Additional PayPal account details
 * @property {string} details.email User's email address
 * @property {string} details.firstName User's given name
 * @property {string} details.lastName User's surname
 * @property {string} details.payerId User's PayPal payerId
 * @property {?string} details.countryCode User's 2 character country code
 * @property {?string} details.phone User's phone number (e.g. 555-867-5309)
 * @property {?object} details.shippingAddress User's shipping address details
 * @property {string} details.shippingAddress.recipientName Recipient of postage
 * @property {string} details.shippingAddress.line1 Street number and name
 * @property {string} details.shippingAddress.line2 Extended address
 * @property {string} details.shippingAddress.city City or locality
 * @property {string} details.shippingAddress.state State or region
 * @property {string} details.shippingAddress.postalCode Postal code
 * @property {string} details.shippingAddress.countryCodeAlpha2 2 character country code (e.g. US)
 */

/**
 * @typedef {object} PayPal~tokenizeReturn
 * @property {Function} close A handle to close the PayPal checkout flow
 */

/**
 * @class
 * @param {module:braintree/paypal~createOptions} options see {@link module:braintree/paypal.create|paypal.create}
 * @classdesc This class represents a PayPal component. Instances of this class have methods for launching auth dialogs and other programmatic interactions with the PayPal component.
 */
function PayPal(options) {
  this._options = options;
  this._assetsUrl = options.client.getConfiguration().gatewayConfiguration.assetsUrl + '/web/' + VERSION + '/html/';
  this._authorizationInProgress = false;
}

PayPal.prototype._initialize = function (callback) {
  var options = this._options;
  var failureTimeout = setTimeout(function () {
    analytics.sendEvent(options.client, 'web.paypal.load.timed-out');
  }, INTEGRATION_TIMEOUT_MS);

  frameService.create(
    this._options,
    {
      name: constants.LANDING_FRAME_NAME,
      url: this._assetsUrl + 'paypal-landing-frame.html'
    },
    function (service) {
      this._frameService = service;
      clearTimeout(failureTimeout);
      analytics.sendEvent(options.client, 'web.paypal.load.succeeded');
      callback();
    }.bind(this)
  );
};

/**
 * Launch the PayPal login flow, returning a nonce payload
 * @public
 * @param {errback} errback The second argument, <code>data</code>, is a {@link PayPal~tokenizePayload|tokenizePayload}
 * @returns {PayPal~tokenizeReturn} A handle to close the PayPal checkout frame
 */
PayPal.prototype.tokenize = function (errback) { //eslint-disable-line
  var client = this._options.client;

  if (this._authorizationInProgress) {
    analytics.sendEvent(client, 'web.paypal.tokenization.error.already-opened');

    errback(new BraintreeError({
      type: BraintreeError.types.MERCHANT,
      message: 'Another tokenization request is active'
    }));
  } else {
    this._authorizationInProgress = true;

    analytics.sendEvent(client, 'web.paypal.tokenization.opened');

    this._frameService.open(function (err) {
      if (err) {
        if (err.message === constants.FRAME_CLOSED_ERROR_MESSAGE) {
          analytics.sendEvent(client, 'web.paypal.tokenization.closed.by-user');
        } else {
          // TODO: consider sending a more explicit error to analytics
          analytics.sendEvent(client, 'web.paypal.tokenization.failed');
        }
      } else {
        analytics.sendEvent(client, 'web.paypal.tokenization.success');
      }

      this._authorizationInProgress = false;

      errback.apply(null, arguments);
    }.bind(this));
  }

  return {
    close: function () {
      analytics.sendEvent(client, 'web.paypal.tokenization.closed.by-merchant');
      this._frameService.close();
    }.bind(this)
  };
};

/**
 * Cleanly tear down anything set up by {@link module:braintree/paypal.create|create}
 * @public
 * @param {errorCallback} errback An errback
 * @returns {void}
 */
PayPal.prototype.teardown = function (errback) {
  this._frameService.teardown();

  analytics.sendEvent(this._options.client, 'web.paypal.teardown-completed');

  errback();
};

module.exports = PayPal;