client/client.js

'use strict';

var request = require('./request');
var isWhitelistedDomain = require('../lib/is-whitelisted-domain');
var BraintreeError = require('../lib/error');
var addMetadata = require('../lib/add-metadata');
var deferred = require('../lib/deferred');
var errors = require('./errors');

/**
 * This object is returned by {@link Client#getConfiguration|getConfiguration}. This information is used extensively by other Braintree modules to properly configure themselves.
 * @typedef {object} Client~configuration
 * @property {object} client The braintree-web/client parameters.
 * @property {string} client.authorization A tokenizationKey or clientToken.
 * @property {object} gatewayConfiguration Gateway-supplied configuration.
 * @property {object} analyticsMetadata Analytics-specific data.
 * @property {string} analyticsMetadata.sessionId Uniquely identifies a browsing session.
 * @property {string} analyticsMetadata.sdkVersion The braintree.js version.
 * @property {string} analyticsMetadata.merchantAppId Identifies the merchant's web app.
 */

/**
 * @class
 * @param {Client~configuration} configuration Options
 * @description <strong>Do not use this constructor directly. Use {@link module:braintree-web/client.create|braintree.client.create} instead.</strong>
 * @classdesc This class is required by many other Braintree components. It serves as the base API layer that communicates with our servers. It is also capable of being used to formulate direct calls to our servers, such as direct credit card tokenization. See {@link Client#request}.
 */
function Client(configuration) {
  var configurationJSON, gatewayConfiguration;

  configuration = configuration || {};

  configurationJSON = JSON.stringify(configuration);
  gatewayConfiguration = configuration.gatewayConfiguration;

  if (!gatewayConfiguration) {
    throw new BraintreeError(errors.CLIENT_MISSING_GATEWAY_CONFIGURATION);
  }

  [
    'assetsUrl',
    'clientApiUrl',
    'configUrl'
  ].forEach(function (property) {
    if (property in gatewayConfiguration && !isWhitelistedDomain(gatewayConfiguration[property])) {
      throw new BraintreeError({
        type: errors.CLIENT_GATEWAY_CONFIGURATION_INVALID_DOMAIN.type,
        code: errors.CLIENT_GATEWAY_CONFIGURATION_INVALID_DOMAIN.code,
        message: property + ' property is on an invalid domain.'
      });
    }
  });

  /**
   * Returns a copy of the configuration values.
   * @public
   * @returns {Client~configuration} configuration
   */
  this.getConfiguration = function () {
    return JSON.parse(configurationJSON);
  };

  this._request = request;
  this._baseUrl = configuration.gatewayConfiguration.clientApiUrl + '/v1/';
  this._configuration = this.getConfiguration();

  this.toJSON = this.getConfiguration;
}

/**
 * Used by other modules to formulate all network requests to the Braintree gateway. It is also capable of being used directly from your own form to tokenize credit card information. However, be sure to satisfy PCI compliance if you use direct card tokenization.
 * @public
 * @param {object} options Request options:
 * @param {string} options.method HTTP method, e.g. "get" or "post".
 * @param {string} options.endpoint Endpoint path, e.g. "payment_methods".
 * @param {object} options.data Data to send with the request.
 * @param {number} [options.timeout=60000] Set a timeout (in milliseconds) for the request.
 * @param {callback} callback The second argument, <code>data</code>, is the returned server data.
 * @example
 * <caption>Direct Credit Card Tokenization</caption>
 * var createClient = require('braintree-web/client').create;
 *
 * createClient({
 *   authorization: CLIENT_AUTHORIZATION
 * }, function (createErr, clientInstance) {
 *   var form = document.getElementById('my-form-id');
 *   var data = {
 *     creditCard: {
 *       number: form['cc-number'].value,
 *       cvv: form['cc-cvv'].value,
 *       expirationDate: form['cc-date'].value,
 *       billingAddress: {
 *         postalCode: form['cc-postal'].value
 *       },
 *       options: {
 *         validate: false
 *       }
 *     }
 *   };
 *
 *   // Warning: For a merchant to be eligible for the easiest level of PCI compliance (SAQ A),
 *   // payment fields cannot be hosted on your checkout page.
 *   // For an alternative to the following, use Hosted Fields.
 *   clientInstance.request({
 *     endpoint: 'payment_methods/credit_cards',
 *     method: 'post',
 *     data: data
 *   }, function (requestErr, response) {
 *     // More detailed example of handling API errors: https://codepen.io/braintree/pen/MbwjdM
 *     if (requestErr) { throw new Error(requestErr); }
 *
 *     console.log('Got nonce:', response.creditCards[0].nonce);
 *   });
 * });
 * @returns {void}
 */
Client.prototype.request = function (options, callback) {
  var optionName;

  if (!options.method) {
    optionName = 'options.method';
  } else if (!options.endpoint) {
    optionName = 'options.endpoint';
  }

  if (optionName) {
    callback = deferred(callback);
    callback(new BraintreeError({
      type: errors.CLIENT_OPTION_REQUIRED.type,
      code: errors.CLIENT_OPTION_REQUIRED.code,
      message: optionName + ' is required when making a request.'
    }));
    return;
  }

  this._request({
    url: this._baseUrl + options.endpoint,
    method: options.method,
    data: addMetadata(this._configuration, options.data),
    // TODO: change this to options.headers and document it
    // when features that require headers are out of beta
    headers: options._headers,
    timeout: options.timeout
  }, this._bindRequestCallback(callback));
};

Client.prototype._bindRequestCallback = function (callback) {
  return function (err, data, status) {
    if (status === -1) {
      callback(new BraintreeError(errors.CLIENT_REQUEST_TIMEOUT), null, status);
    } else if (status === 403) {
      callback(new BraintreeError(errors.CLIENT_AUTHORIZATION_INSUFFICIENT), null, status);
    } else if (status === 429) {
      callback(new BraintreeError(errors.CLIENT_RATE_LIMITED), null, status);
    } else if (status >= 500) {
      callback(new BraintreeError(errors.CLIENT_GATEWAY_NETWORK), null, status);
    } else if (status < 200 || status >= 400) {
      callback(new BraintreeError({
        type: errors.CLIENT_REQUEST_ERROR.type,
        code: errors.CLIENT_REQUEST_ERROR.code,
        message: errors.CLIENT_REQUEST_ERROR.message,
        details: {originalError: err}
      }), null, status);
    } else {
      callback(null, data, status);
    }
  };
};

module.exports = Client;