payment-ready/payment-ready.js

"use strict";

var wrapPromise = require("@braintree/wrap-promise");

var analytics = require("../lib/analytics");
var BraintreeError = require("../lib/braintree-error");
var constants = require("./constants");
var errors = require("./errors");
var hasMissingOption = require("../lib/has-missing-option");

/**
 * @class
 * @param {object} options see {@link module:braintree-web/payment-ready.create|payment-ready.create}
 * @description <strong>Do not use this constructor directly. Use {@link module:braintree-web/payment-ready.create|braintree-web.payment-ready.create} instead.</strong>
 * @classdesc This class represents a Payment Ready component produced by {@link module:braintree-web/payment-ready.create|braintree-web.payment-ready.create}. Instances provide methods for Payment Ready methods.
 */
function PaymentReady(options) {
  this._instantiatedWithClient = Boolean(!options.useDeferredClient);
  this._client = options.client;
  this._createPromise = options.createPromise;
}

/**
 * Payment Ready Customer data object.
 * @typedef {object} PaymentReady~Customer
 * @property {string} [deviceFingerprintId] - Unique device identifier
 * @property {string} [hashedEmail] - This field must contain a Hex-encoded SHA-256 hashed value of the actual email.
 * The email address must be sanitized before hashing:
 *  * email address in lowercase
 *  * remove leading and trailing whitespace
 * @property {string} [hashedPhoneNumber] - This field must contain a Hex-encoded SHA-256 hashed value of the phone number; and the phone number must be in E.164 format.
 * * E.164 format example 1: '+12345551234' where country code is '1', area code is '234' and subscriber number is '555-1234'
 * * E.164 format example 2: '+6129876543X' where country code is '61', area code is '2' and subscriber number is '9876 543X'
 * @property {boolean} [paypalAppInstalled] - A flag indicating if customer device has paypal application installed.
 * @property {boolean} [venmoAppInstalled] - A flag indicating if customer device has venmo applicaition installed.
 * @property {string} [userAgent] - The user agent string of customer browser describing application, OS, vendor, and version of requesting user agent.
 */

/**
 * Creates a customer session using the Payment Ready API.
 *
 * @public
 * @param {object} options - The options for creating a customer session.
 * @param {PaymentReady~Customer} options.customer - The customer data object. At minimum, this object should contain  either `hashedEmail` or `hashedPhoneNumber` field.
 * @param {string} [options.sessionId] - Optionally, specify a unique identifier for this customer session. It must be at least 36 characters, no more than 100 characters and match the following regex: `[A-Za-z0-9-_.]+`.
 * @returns {Promise<object>} A promise that resolves with the created customer session data.
 * @throws {BraintreeError} If the specified `sessionId` has previously been used or has an invalid format.
 * @example
 * paymentReadyInstance.createCustomerSession({
 *   customer: {
 *     ... // Customer data
 *   }
 * }).then(function (sessionData) {
 *   // Handle the session data
 *   console.log(sessionData.sessionId);
 * }).catch(function (err) {
 *   // Handle errors
 *   console.error(err);
 * });
 */
PaymentReady.prototype.createCustomerSession = function (options) {
  var self = this;

  if (
    !options ||
    hasMissingOption(options, constants.REQUIRED_OPTIONS_CREATE_SESSION)
  ) {
    analytics.sendEvent(
      self._client,
      "payment-ready.create-customer-session.missing-options"
    );

    return Promise.reject(
      new BraintreeError(errors.PAYMENT_READY_MISSING_REQUIRED_OPTION)
    );
  }

  return self._client
    .request({
      api: "graphQLApi",
      data: {
        query: constants.CREATE_PAYMENT_READY_SESSION_QUERY,
        variables: {
          input: {
            sessionId: options.sessionId,
            customer: options.customer,
          },
        },
      },
    })
    .then(function (response) {
      analytics.sendEvent(
        self._client,
        "payment-ready.create-customer-session.succeeded"
      );

      return response.data.createCustomerSession;
    })
    .catch(function (err) {
      analytics.sendEvent(
        self._client,
        "payment-ready.create-customer-session.failed"
      );

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

/**
 * Updates an existing customer session using the Payment Ready API.
 *
 * @public
 * @param {object} options - The options for updating a customer session.
 * @param {string} options.sessionId - The ID of the session to update.
 * @param {PaymentReady~Customer} options.customer - The updated customer data object.
 * @returns {Promise<object>} A promise that resolves with the updated customer session data.
 * @example
 * paymentReadyInstance.updateCustomerSession({
 *   sessionId: "session-abc-123",
 *   customer: {
 *     ... // Updated customer data
 *   }
 * }).then(function (sessionData) {
 *   // Handle the updated session data
 *   console.log(sessionData.sessionId);
 * }).catch(function (err) {
 *   // Handle errors
 *   console.error(err);
 * });
 */
PaymentReady.prototype.updateCustomerSession = function (options) {
  var self = this;

  if (
    !options ||
    hasMissingOption(options, constants.REQUIRED_OPTIONS_UPDATE_SESSION)
  ) {
    analytics.sendEvent(
      self._client,
      "payment-ready.update-customer-session.missing-options"
    );

    return Promise.reject(
      new BraintreeError(errors.PAYMENT_READY_MISSING_REQUIRED_OPTION)
    );
  }

  return self._client
    .request({
      api: "graphQLApi",
      data: {
        query: constants.UPDATE_PAYMENT_READY_SESSION_QUERY,
        variables: {
          input: {
            sessionId: options.sessionId,
            customer: options.customer,
          },
        },
      },
    })
    .then(function (response) {
      analytics.sendEvent(
        self._client,
        "payment-ready.update-customer-session.succeeded"
      );

      return response.data.updateCustomerSession;
    })
    .catch(function (err) {
      analytics.sendEvent(
        self._client,
        "payment-ready.update-customer-session.failed"
      );

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

/**
 * Generates customer payment recommendations using the Payment Ready API.
 *
 * @public
 * @param {object} options - The options for generating customer recommendations.
 * @param {string} options.sessionId - The ID of the session to get recommendations for.
 * @param {PaymentReady~Customer} [options.customer] - Optional customer data object.
 * @param {string} [options.domain] - Optional domain parameter.
 * @param {Array} [options.purchaseUnits] - Optional purchase units array.
 * @returns {Promise<object>} A promise that resolves with the customer recommendations data.
 * @example
 * paymentReadyInstance.getCustomerRecommendations({
 *   sessionId: "session-abc-123",
 *   customer: {
 *     // Optional customer data
 *   },
 *   domain: "example.com",
 *   purchaseUnits: [
 *     // Purchase unit data
 *   ]
 * }).then(function (recommendations) {
 *   // Handle the recommendations data
 *   console.log(recommendations.paymentRecommendations);
 * }).catch(function (err) {
 *   // Handle errors
 *   console.error(err);
 * });
 */
PaymentReady.prototype.getCustomerRecommendations = function (options) {
  var self = this;

  if (
    !options ||
    hasMissingOption(options, constants.REQUIRED_OPTIONS_GET_RECOMMENDATIONS)
  ) {
    analytics.sendEvent(
      self._client,
      "payment-ready.get-customer-recommendations.missing-options"
    );

    return Promise.reject(
      new BraintreeError(errors.PAYMENT_READY_NO_SESSION_ID)
    );
  }

  return self._client
    .request({
      api: "graphQLApi",
      data: {
        query: constants.GENERATE_CUSTOMER_RECOMMENDATIONS_QUERY,
        variables: {
          input: {
            sessionId: options.sessionId,
            customer: options.customer,
            domain: options.domain,
            purchaseUnits: options.purchaseUnits,
          },
        },
      },
    })
    .then(function (response) {
      analytics.sendEvent(
        self._client,
        "payment-ready.get-customer-recommendations.succeeded"
      );

      return response.data.generateCustomerRecommendations;
    })
    .catch(function (err) {
      analytics.sendEvent(
        self._client,
        "payment-ready.get-customer-recommendations.failed"
      );

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

/**
 * Call this method when a payment button has been successfully displayed to the buyer.
 * This method sends analytics to help improve the Shopper Insights feature experience.
 *
 * @public
 * @param {object} options - The options for the event.
 * @param {string} options.buttonType Type of button presented to the user.
 * @param {string} options.paymentReadySessionId - The id of paymentReady session created with createCustomerSession method.
 * @param {object} [options.presentmentDetails] Details of presentment. Includes experimentType, pageType, and buttonOrder.
 * @param {PaymentReady~EXPERIMENT_TYPE} [options.presentmentDetails.experimentType] Experiment type being used.
 * @param {PaymentReady~PAGE_TYPE} [options.presentmentDetails.pageType] Page type where the SDK is being used.
 * @param {PaymentReady~BUTTON_ORDER} [options.presentmentDetails.buttonOrder] Where in the list of buttons this button was presented.
 *
 * @example
 * paymentReadyInstance.sendPresentedEvent({
 *   buttonType: "paypal",
 *   paymentReadySessionId: "SESSION_ID_123",
 *   presentmentDetails: {
 *     experimentType: "control",
 *     pageType: "checkout",
 *     buttonOrder: "second"
 * });
 * @returns {void} This method does not return any value.
 */
PaymentReady.prototype.sendPresentedEvent = function (options) {
  var experimentType = options.presentmentDetails
    ? options.presentmentDetails.experimentType
    : null;
  var pageType = options.presentmentDetails
    ? options.presentmentDetails.pageType
    : null;
  var buttonOrder = options.presentmentDetails
    ? options.presentmentDetails.buttonOrder
    : null;

  if (
    !options.paymentReadySessionId ||
    typeof options.paymentReadySessionId !== "string"
  ) {
    // eslint-disable-next-line no-console
    console.warn(
      "sendPresentedEvent: paymentReadySessionId is missing or invalid"
    );

    return;
  }

  if (!options.buttonType || typeof options.buttonType !== "string") {
    // eslint-disable-next-line no-console
    console.warn("sendPresentedEvent: buttonType is missing");

    return;
  }

  var buttonSelected = options.buttonType;
  var presentAnalyticFields;

  if (!Object.values(constants.BUTTON_TYPE).includes(buttonSelected)) {
    buttonSelected = constants.BUTTON_TYPE.OTHER;
  }

  if (
    buttonOrder &&
    !Object.values(constants.BUTTON_ORDER).includes(buttonOrder)
  ) {
    buttonOrder = constants.BUTTON_ORDER.OTHER;
  }

  if (
    experimentType &&
    !Object.values(constants.EXPERIMENT_TYPE).includes(experimentType)
  ) {
    experimentType = constants.EXPERIMENT_TYPE.TEST;
  }

  if (pageType && !Object.values(constants.PAGE_TYPE).includes(pageType)) {
    pageType = constants.PAGE_TYPE.OTHER;
  }

  presentAnalyticFields = buildPresentmentAnalyticFields({
    buttonOrder: buttonOrder,
    buttonType: buttonSelected,
    experimentType: experimentType,
    pageType: pageType,
    sessionId: options.paymentReadySessionId,
  });

  analytics.sendEventPlus(
    this._client,
    constants.EVENT_BUTTON_PRESENTED,
    presentAnalyticFields
  );
};

function buildPresentmentAnalyticFields(options) {
  var pkg = {
    // eslint-disable-next-line camelcase
    button_type: options.buttonType,
    // eslint-disable-next-line camelcase
    payment_ready_session_id: options.sessionId,
  };
  if (options.buttonOrder) {
    pkg["payment_ready_button_order"] = options.buttonOrder;
  }
  if (options.experimentType) {
    pkg["payment_ready_experiment_type"] = options.experimentType;
  }
  if (options.pageType) {
    pkg["payment_ready_page_type"] = options.pageType;
  }

  return pkg;
}
/**
 * Use PayPal Payment Ready to optimize your checkout experience by sending analytics events.
 *
 * @public
 * @param {object} options - The options for the event
 * @param {string} options.paymentReadySessionId - The ID of the session to update.
 * @param {PaymentReady~BUTTON_TYPE} options.buttonType Type of button presented to the user.
 * @returns {void}
 * @example
 * <caption>Sending analytics event</caption>
 * paymentReadyInstance.sendSelectedEvent({
 *   paymentReadySessionId: "session-abc-123",
 *   buttonType: "paypal"
 * });
 */
PaymentReady.prototype.sendSelectedEvent = function (options) {
  if (
    !options.paymentReadySessionId ||
    typeof options.paymentReadySessionId !== "string"
  ) {
    // eslint-disable-next-line no-console
    console.warn(
      "sendSelectedEvent: paymentReadySessionId is missing or invalid"
    );

    return;
  }
  if (!options.buttonType || typeof options.buttonType !== "string") {
    // eslint-disable-next-line no-console
    console.warn("sendSelectedEvent: buttonType is missing");

    return;
  }

  var buttonSelected = options.buttonType;

  if (!Object.values(constants.BUTTON_TYPE).includes(buttonSelected)) {
    buttonSelected = constants.BUTTON_TYPE.OTHER;
  }

  analytics.sendEventPlus(this._client, constants.EVENT_BUTTON_SELECTED, {
    // eslint-disable-next-line camelcase
    payment_ready_session_id: options.paymentReadySessionId,
    // eslint-disable-next-line camelcase
    button_type: buttonSelected,
  });
};

module.exports = wrapPromise.wrapPrototype(PaymentReady);