apple-pay/apple-pay.js

'use strict';

var BraintreeError = require('../lib/error');
var analytics = require('../lib/analytics');
var deferred = require('../lib/deferred');
var sharedErrors = require('../errors');
var errors = require('./errors');

/**
 * An Apple Pay Payment Authorization Event object.
 * @external ApplePayPaymentAuthorizedEvent
 * @see {@link https://developer.apple.com/reference/applepayjs/applepaypaymentauthorizedevent ApplePayPaymentAuthorizedEvent}
 */

/**
 * An Apple Pay Payment Request object.
 * @external ApplePayPaymentRequest
 * @see {@link https://developer.apple.com/reference/applepayjs/1916082-applepay_js_data_types/paymentrequest PaymentRequest}
 */

/**
 * @class
 * @param {object} options Options
 * @description <strong>You cannot use this constructor directly. Use {@link module:braintree-web/apple-pay.create|braintree.apple-pay.create} instead.</strong>
 * @classdesc This class represents an Apple Pay component. Instances of this class have methods for validating the merchant server and tokenizing payments.
 */
function ApplePay(options) {
  this._client = options.client;
  Object.defineProperty(this, 'merchantIdentifier', {
    value: this._client.getConfiguration().gatewayConfiguration.applePay.merchantIdentifier,
    configurable: false,
    writable: false
  });
}

/**
 * Merges a payment request with Braintree defaults
 * The following properties are assigned to `paymentRequest` if not already defined
 * - countryCode
 * - currencyCode
 * - merchantCapabilities
 * - supportedNetworks
 * @public
 * @param {external:ApplePayPaymentRequest} paymentRequest The payment request details to apply on top of those from Braintree.
 * @returns {external:ApplePayPaymentRequest} The decorated `paymentRequest`.
 * @example
 * var applePay = require('braintree-web/apple-pay');
 *
 * applePay.create({client: clientInstance}, function (createErr, applePayInstance) {
 *   // ...
 *   var paymentRequest = applePay.createPaymentRequest({
 *    total: {
 *      label: 'My Company',
 *      amount: '19.99'
 *   });
 *
 *   console.log(paymentRequest);
 *   // { total: { }, countryCode: 'US', currencyCode: 'USD', merchantCapabilities: [ ], supportedNetworks: [ ] }
 *
 */
ApplePay.prototype.createPaymentRequest = function (paymentRequest) {
  var applePay = this._client.getConfiguration().gatewayConfiguration.applePay;
  var defaults = {
    countryCode: applePay.countryCode,
    currencyCode: applePay.currencyCode,
    merchantCapabilities: applePay.merchantCapabilities || ['supports3DS'],
    supportedNetworks: applePay.supportedNetworks.map(function (network) {
      return network === 'mastercard' ? 'masterCard' : network;
    })
  };

  return Object.assign({}, defaults, paymentRequest);
};

/**
 * Validates the merchant website, as required by ApplePaySession before payment can be authorized.
 * @public
 * @param {object} options Options
 * @param {string} options.validationURL The validationURL fram an ApplePayValidateMerchantEvent.
 * @param {string} [options.displayName]
 * - The canonical name for your store.
 * - The system may display this name to the user.
 * - Use a 128-character or less, UTF-8 string.
 * - Do not localize the name.
 * @param {string} [options.merchantIdentifier]
 * Your Apple merchant identifier. This is the Apple Merchant ID created on the Apple Developer Portal.
 * Defaults to the merchant identifier specified in the Braintree Control Panel.
 * You can use this field to override the merchant identifier for this transaction.
 * @param {callback} callback The second argument, <code>data</code>, is the Apple Pay merchant session object.
 * Pass the merchant session to your Apple Pay session's completeMerchantValidation method.
 * @returns {void}
 * @example
 * var applePay = require('braintree-web/apple-pay');
 *
 * applePay.create({client: clientInstance}, function (createErr, applePayInstance) {
 *   var session = new ApplePaySession(1, {
 *     // This should be the payment request object that
 *     // contains the information needed to display the payment sheet.
 *   });
 *
 *   session.onvalidatemerchant = function (event) {
 *     applePay.performValidation({
 *       validationURL: event.validationURL
 *     }, function(err, validationData) {
 *       if (err) {
 *         console.error(err);
 *         session.abort();
 *         return;
 *       }
 *       session.completeMerchantValidation(validationData);
 *     });
 *   };
 * });
 */
ApplePay.prototype.performValidation = function (options, callback) {
  var applePayWebSession;

  if (typeof callback !== 'function') {
    throw new BraintreeError({
      type: sharedErrors.CALLBACK_REQUIRED.type,
      code: sharedErrors.CALLBACK_REQUIRED.code,
      message: 'performValidation requires a callback.'
    });
  }

  callback = deferred(callback);

  if (!options || !options.validationURL) {
    callback(new BraintreeError(errors.APPLE_PAY_VALIDATION_URL_REQUIRED));
    return;
  }

  applePayWebSession = {
    validationUrl: options.validationURL,
    domainName: options.domainName || global.location.hostname,
    merchantIdentifier: options.merchantIdentifier || this.merchantIdentifier
  };

  if (options.displayName != null) {
    applePayWebSession.displayName = options.displayName;
  }

  this._client.request({
    method: 'post',
    endpoint: 'apple_pay_web/sessions',
    data: {
      _meta: {source: 'apple-pay'},
      applePayWebSession: applePayWebSession
    }
  }, function (err, response) {
    if (err) {
      if (err.code === 'CLIENT_REQUEST_ERROR') {
        callback(new BraintreeError({
          type: errors.APPLE_PAY_MERCHANT_VALIDATION_FAILED.type,
          code: errors.APPLE_PAY_MERCHANT_VALIDATION_FAILED.code,
          message: errors.APPLE_PAY_MERCHANT_VALIDATION_FAILED.message,
          details: {
            originalError: err.details.originalError
          }
        }));
      } else {
        callback(new BraintreeError({
          type: errors.APPLE_PAY_MERCHANT_VALIDATION_NETWORK.type,
          code: errors.APPLE_PAY_MERCHANT_VALIDATION_NETWORK.code,
          message: errors.APPLE_PAY_MERCHANT_VALIDATION_NETWORK.message,
          details: {
            originalError: err
          }
        }));
      }
      analytics.sendEvent(this._client, 'applepay.performValidation.failed');
    } else {
      callback(null, response);
      analytics.sendEvent(this._client, 'applepay.performValidation.succeeded');
    }
  }.bind(this));
};

/**
 * Tokenizes an Apple Pay payment.
 * @public
 * @param {object} options Options
 * @param {object} options.token The `payment.token` property of an {@link external:ApplePayPaymentAuthorizedEvent}
 * @param {callback} callback The second argument, <code>data</code>, is the tokenized payload.
 * @returns {void}
 * @example
 * var applePay = require('braintree-web/apple-pay');
 *
 * applePay.create({client: clientInstance}, function (createErr, applePayInstance) {
 *   var session = new ApplePaySession(1, { });
 *
 *   session.onpaymentauthorized = function (event) {
 *     applePay.tokenize({
 *       token: event.payment.token
 *     }, function (err, tokenizedPayload) {
 *       if (err) {
 *         session.completePayment(ApplePaySession.STATUS_FAILURE);
 *         return;
 *       }
 *       session.completePayment(ApplePaySession.STATUS_SUCCESS);
 *
 *       // Send the tokenizedPayload to your server.
 *     });
 *  };
 * });
 */
ApplePay.prototype.tokenize = function (options, callback) {
  if (typeof callback !== 'function') {
    throw new BraintreeError({
      type: sharedErrors.CALLBACK_REQUIRED.type,
      code: sharedErrors.CALLBACK_REQUIRED.code,
      message: 'tokenize requires a callback.'
    });
  }

  callback = deferred(callback);

  if (!options.token) {
    callback(new BraintreeError(errors.APPLE_PAY_PAYMENT_TOKEN_REQUIRED));
    return;
  }

  this._client.request({
    method: 'post',
    endpoint: 'payment_methods/apple_payment_tokens',
    data: {
      _meta: {
        source: 'apple-pay'
      },
      applePaymentToken: Object.assign({}, options.token, {
        // The gateway requires this key to be base64-encoded.
        paymentData: btoa(JSON.stringify(options.token.paymentData))
      })
    }
  }, function (err, response) {
    if (err) {
      callback(new BraintreeError({
        type: errors.APPLE_PAY_TOKENIZATION.type,
        code: errors.APPLE_PAY_TOKENIZATION.code,
        message: errors.APPLE_PAY_TOKENIZATION.message,
        details: {
          originalError: err
        }
      }));
      analytics.sendEvent(this._client, 'applepay.tokenize.failed');
    } else {
      callback(null, response.applePayCards[0]);
      analytics.sendEvent(this._client, 'applepay.tokenize.succeeded');
    }
  }.bind(this));
};

module.exports = ApplePay;