apple-pay/apple-pay.js

"use strict";

var BraintreeError = require("../lib/braintree-error");
var analytics = require("../lib/analytics");
var errors = require("./errors");
var methods = require("../lib/methods");
var convertMethodsToError = require("../lib/convert-methods-to-error");
var wrapPromise = require("@braintree/wrap-promise");

/**
 * @typedef {object} ApplePay~tokenizePayload
 * @property {string} nonce The payment method nonce.
 * @property {object} details Additional details.
 * @property {string} details.cardType Type of card, ex: Visa, MasterCard.
 * @property {string} details.cardHolderName The name of the card holder.
 * @property {string} details.dpanLastTwo Last two digits of card number.
 * @property {string} description A human-readable description.
 * @property {string} type The payment method type, always `ApplePayCard`.
 * @property {object} binData Information about the card based on the bin.
 * @property {string} binData.commercial Possible values: 'Yes', 'No', 'Unknown'.
 * @property {string} binData.countryOfIssuance The country of issuance.
 * @property {string} binData.debit Possible values: 'Yes', 'No', 'Unknown'.
 * @property {string} binData.durbinRegulated Possible values: 'Yes', 'No', 'Unknown'.
 * @property {string} binData.healthcare Possible values: 'Yes', 'No', 'Unknown'.
 * @property {string} binData.issuingBank The issuing bank.
 * @property {string} binData.payroll Possible values: 'Yes', 'No', 'Unknown'.
 * @property {string} binData.prepaid Possible values: 'Yes', 'No', 'Unknown'.
 * @property {string} binData.productId The product id.
 */

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

/**
 * An Apple Pay Payment Request object.
 * @typedef {object} ApplePayPaymentRequest
 * @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.applePay.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._instantiatedWithClient = Boolean(!options.useDeferredClient);
  this._client = options.client;
  this._createPromise = options.createPromise;

  if (this._client) {
    this._setMerchantIdentifier();
  }
}

ApplePay.prototype._waitForClient = function () {
  if (this._client) {
    return Promise.resolve();
  }

  return this._createPromise.then(
    function (client) {
      this._client = client;

      this._setMerchantIdentifier();
    }.bind(this)
  );
};

ApplePay.prototype._setMerchantIdentifier = function () {
  var applePayConfig =
    this._client.getConfiguration().gatewayConfiguration.applePayWeb;

  if (!applePayConfig) {
    return;
  }
  /**
   * @name ApplePay#merchantIdentifier
   * @description A special merchant ID which represents the merchant association with Braintree. Required when using `ApplePaySession.canMakePaymentsWithActiveCard`.
   * @example
   * var promise = ApplePaySession.canMakePaymentsWithActiveCard(applePayInstance.merchantIdentifier);
   * promise.then(function (canMakePaymentsWithActiveCard) {
   *   if (canMakePaymentsWithActiveCard) {
   *     // Set up Apple Pay buttons
   *   }
   * });
   */
  Object.defineProperty(this, "merchantIdentifier", {
    value: applePayConfig.merchantIdentifier,
    configurable: false,
    writable: false,
  });
};

/**
 * Merges a payment request with Braintree defaults to return an {external:ApplePayPaymentRequest}.
 *
 * The following properties are assigned to `paymentRequest` if not already defined. Their default values come from the Braintree gateway.
 * - `countryCode`
 * - `currencyCode`
 * - `merchantCapabilities`
 * - `supportedNetworks`
 * @public
 * @param {external:ApplePayPaymentRequest} paymentRequest The payment request details to apply on top of those from Braintree.
 * @returns {external:ApplePayPaymentRequest|Promise} The decorated `paymentRequest` object. If `useDeferredClient` is used along with an `authorization`, this method will return a promise that resolves with the `paymentRequest` object.
 * @example
 * var applePay = require('braintree-web/apple-pay');
 *
 * applePay.create({client: clientInstance}, function (applePayErr, applePayInstance) {
 *   if (applePayErr) {
 *     // Handle error here
 *     return;
 *   }
 *
 *   var paymentRequest = applePayInstance.createPaymentRequest({
 *     total: {
 *       label: 'My Company',
 *       amount: '19.99'
 *     }
 *   });
 *
 *   var session = new ApplePaySession(3, paymentRequest);
 *
 *   // ...
 * @example <caption>With deferred client</caption>
 * var applePay = require('braintree-web/apple-pay');
 *
 * applePay.create({
 *   authorization: 'client-token-or-tokenization-key',
 *   useDeferredClient: true
 * }, function (applePayErr, applePayInstance) {
 *   if (applePayErr) {
 *     // Handle error here
 *     return;
 *   }
 *
 *   applePayInstance.createPaymentRequest({
 *     total: {
 *       label: 'My Company',
 *       amount: '19.99'
 *     }
 *   }).then(function (paymentRequest) {
 *     var session = new ApplePaySession(3, paymentRequest);
 *
 *     // ...
 *   });
 */
ApplePay.prototype.createPaymentRequest = function (paymentRequest) {
  if (this._instantiatedWithClient) {
    return this._createPaymentRequestSynchronously(paymentRequest);
  }

  return this._waitForClient().then(
    function () {
      return this._createPaymentRequestSynchronously(paymentRequest);
    }.bind(this)
  );
};

ApplePay.prototype._createPaymentRequestSynchronously = function (
  paymentRequest
) {
  var applePay =
    this._client.getConfiguration().gatewayConfiguration.applePayWeb;
  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 your merchant website, as required by `ApplePaySession` before payment can be authorized.
 * @public
 * @param {object} options Options
 * @param {string} options.validationURL The validationURL from an `ApplePayValidateMerchantEvent`.
 * @param {string} options.displayName The canonical name for your store. Use a non-localized name. This parameter should be a UTF-8 string that is a maximum of 64 characters. The system may display this name to the user.
 * @param {callback} [callback] The second argument, <code>data</code>, is the Apple Pay merchant session object. If no callback is provided, `performValidation` returns a promise.
 * Pass the merchant session to your Apple Pay session's `completeMerchantValidation` method.
 * @returns {(Promise|void)} Returns a promise if no callback is provided.
 * @example
 * var applePay = require('braintree-web/apple-pay');
 *
 * applePay.create({client: clientInstance}, function (applePayErr, applePayInstance) {
 *   if (applePayErr) {
 *     // Handle error here
 *     return;
 *   }
 *
 *   var paymentRequest = applePayInstance.createPaymentRequest({
 *     total: {
 *       label: 'My Company',
 *       amount: '19.99'
 *     }
 *   });
 *   var session = new ApplePaySession(3, paymentRequest);
 *
 *   session.onvalidatemerchant = function (event) {
 *     applePayInstance.performValidation({
 *       validationURL: event.validationURL,
 *       displayName: 'My Great Store'
 *     }, function (validationErr, validationData) {
 *       if (validationErr) {
 *         console.error(validationErr);
 *         session.abort();
 *         return;
 *       }
 *
 *       session.completeMerchantValidation(validationData);
 *     });
 *   };
 * });
 */
ApplePay.prototype.performValidation = function (options) {
  var self = this;

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

  return this._waitForClient()
    .then(function () {
      var applePayWebSession = {
        validationUrl: options.validationURL,
        domainName: options.domainName || window.location.hostname,
        merchantIdentifier:
          options.merchantIdentifier || self.merchantIdentifier,
      };

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

      return self._client.request({
        method: "post",
        endpoint: "apple_pay_web/sessions",
        data: {
          _meta: { source: "apple-pay" },
          applePayWebSession: applePayWebSession,
        },
      });
    })
    .then(function (response) {
      analytics.sendEvent(self._client, "applepay.performValidation.succeeded");

      return Promise.resolve(response);
    })
    .catch(function (err) {
      analytics.sendEvent(self._client, "applepay.performValidation.failed");

      if (err.code === "CLIENT_REQUEST_ERROR") {
        return Promise.reject(
          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,
            },
          })
        );
      }

      return Promise.reject(
        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,
          },
        })
      );
    });
};

/**
 * Tokenizes an Apple Pay payment. This will likely be called in your `ApplePaySession`'s `onpaymentauthorized` callback.
 * @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 a {@link ApplePay~tokenizePayload|tokenizePayload}. If no callback is provided, `tokenize` returns a promise that resolves with a {@link ApplePay~tokenizePayload|tokenizePayload}.
 * @returns {(Promise|void)} Returns a promise if no callback is provided.
 * @example
 * var applePay = require('braintree-web/apple-pay');
 *
 * applePay.create({client: clientInstance}, function (applePayErr, applePayInstance) {
 *   if (applePayErr) {
 *     // Handle error here
 *     return;
 *   }
 *
 *   var paymentRequest = applePayInstance.createPaymentRequest({
 *     total: {
 *       label: 'My Company',
 *       amount: '19.99'
 *     }
 *   });
 *   var session = new ApplePaySession(3, paymentRequest);
 *
 *   session.onpaymentauthorized = function (event) {
 *     applePayInstance.tokenize({
 *       token: event.payment.token
 *     }, function (tokenizeErr, tokenizedPayload) {
 *       if (tokenizeErr) {
 *         session.completePayment(ApplePaySession.STATUS_FAILURE);
 *         return;
 *       }
 *       // Send the tokenizedPayload to your server here!
 *
 *       // Once the transaction is complete, call completePayment
 *       // to close the Apple Pay sheet
 *       session.completePayment(ApplePaySession.STATUS_SUCCESS);
 *     });
 *   };
 *
 *   // ...
 * });
 */
ApplePay.prototype.tokenize = function (options) {
  var self = this;

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

  return this._waitForClient()
    .then(function () {
      return self._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)),
          }),
        },
      });
    })
    .then(function (response) {
      analytics.sendEvent(self._client, "applepay.tokenize.succeeded");

      return Promise.resolve(response.applePayCards[0]);
    })
    .catch(function (err) {
      analytics.sendEvent(self._client, "applepay.tokenize.failed");

      return Promise.reject(
        new BraintreeError({
          type: errors.APPLE_PAY_TOKENIZATION.type,
          code: errors.APPLE_PAY_TOKENIZATION.code,
          message: errors.APPLE_PAY_TOKENIZATION.message,
          details: {
            originalError: err,
          },
        })
      );
    });
};

/**
 * Cleanly tear down anything set up by {@link module:braintree-web/apple-pay.create|create}.
 * @public
 * @param {callback} [callback] Called once teardown is complete. No data is returned if teardown completes successfully.
 * @example
 * applePayInstance.teardown();
 * @example <caption>With callback</caption>
 * applePayInstance.teardown(function () {
 *   // teardown is complete
 * });
 * @returns {(Promise|void)} Returns a promise if no callback is provided.
 */
ApplePay.prototype.teardown = function () {
  convertMethodsToError(this, methods(ApplePay.prototype));

  return Promise.resolve();
};

module.exports = wrapPromise.wrapPrototype(ApplePay);