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');
var methods = require('../../lib/methods');
var convertMethodsToError = require('../../lib/convert-methods-to-error');

/**
 * @typedef {object} PayPal~tokenizePayload
 * @property {string} nonce The payment method nonce
 * @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.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, only available if shipping address is enabled
 * @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.countryCode 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-web/paypal~createOptions} options see {@link module:braintree-web/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.paypal.assetsUrl + '/web/' + VERSION;
  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,
      landingFrameHTML: '/html/paypal-landing-frame@DOT_MIN.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} callback 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 (callback) { //eslint-disable-line
  var client;

  if (typeof callback !== 'function') {
    throw new BraintreeError({
      type: BraintreeError.types.MERCHANT,
      message: 'tokenize must include a callback function'
    });
  }

  client = this._options.client;

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

    callback(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;

      callback.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-web/paypal.create|create}
 * @public
 * @param {errback} [callback] Called once teardown is complete. No data is returned if teardown completes successfully.
 * @returns {void}
 */
PayPal.prototype.teardown = function (callback) {
  this._frameService.teardown();

  convertMethodsToError(this, methods(PayPal.prototype));

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

  if (typeof callback === 'function') {
    callback();
  }
};

module.exports = PayPal;