paypal-checkout-v6/paypal-checkout-v6.js

"use strict";

var analytics = require("../lib/analytics");
var assign = require("../lib/assign").assign;
var BraintreeError = require("../lib/braintree-error");
var convertToBraintreeError = require("../lib/convert-to-braintree-error");
var createDeferredClient = require("../lib/create-deferred-client");
var createAssetsUrl = require("../lib/create-assets-url");
var createAuthorizationData = require("../lib/create-authorization-data");
var errors = require("./errors");
var frameService = require("../lib/frame-service/external");
var methods = require("../lib/methods");
var convertMethodsToError = require("../lib/convert-methods-to-error");
var useMin = require("../lib/use-min");
var wrapPromise = require("@braintree/wrap-promise");
var ExtendedPromise = require("@braintree/extended-promise");
var constants = require("./constants");
var INTEGRATION_TIMEOUT_MS = require("../lib/constants").INTEGRATION_TIMEOUT_MS;

ExtendedPromise.suppressUnhandledPromiseMessage = true;

/**
 * @class
 * @param {object} options see {@link module:braintree-web/paypal-checkout-v6.create|paypal-checkout-v6.create}
 * @classdesc This class represents a PayPal Checkout V6 component that coordinates with the PayPal Web SDK v6. Instances of this class can load the PayPal SDK, create payment sessions, and tokenize payments.
 * @description <strong>Do not use this constructor directly. Use {@link module:braintree-web/paypal-checkout-v6.create|braintree-web.paypal-checkout-v6.create} instead.</strong>
 *
 * #### Integrate One-Time Payment Flow with PayPal V6
 *
 * ```javascript
 * braintree.client.create({
 *   authorization: 'client-token'
 * }).then(function (clientInstance) {
 *   return braintree.paypalCheckoutV6.create({
 *     client: clientInstance
 *   });
 * }).then(function (paypalCheckoutV6Instance) {
 *   // Load the PayPal V6 SDK
 *   return paypalCheckoutV6Instance.loadPayPalSDK();
 * }).then(function (paypalCheckoutV6Instance) {
 *   // Create a one-time payment session
 *   var session = paypalCheckoutV6Instance.createOneTimePaymentSession({
 *     amount: '10.00',
 *     currency: 'USD',
 *     intent: 'capture',
 *
 *     onApprove: function (data) {
 *       return paypalCheckoutV6Instance.tokenizePayment(data).then(function (payload) {
 *         // Submit payload.nonce to your server
 *       });
 *     },
 *
 *     onCancel: function () {
 *       // Handle case where user cancels
 *     },
 *
 *     onError: function (err) {
 *       // Handle case where error occurs
 *     }
 *   });
 *
 *   // Trigger the payment flow when user clicks a button
 *   document.getElementById('paypal-button').addEventListener('click', function () {
 *     session.start();
 *   });
 * }).catch(function (err) {
 *  console.error('Error!', err);
 * });
 * ```
 *
 * #### Integrate Vault Flow (Billing Agreement) with PayPal V6
 *
 * ```javascript
 * braintree.client.create({
 *   authorization: 'client-token'
 * }).then(function (clientInstance) {
 *   return braintree.paypalCheckoutV6.create({
 *     client: clientInstance
 *   });
 * }).then(function (paypalCheckoutV6Instance) {
 *   // Load the PayPal V6 SDK
 *   return paypalCheckoutV6Instance.loadPayPalSDK();
 * }).then(function (paypalCheckoutV6Instance) {
 *   // Create a billing agreement session
 *   var session = paypalCheckoutV6Instance.createBillingAgreementSession({
 *     billingAgreementDescription: 'Monthly subscription',
 *
 *     onApprove: function (data) {
 *       return paypalCheckoutV6Instance.tokenizePayment(data).then(function (payload) {
 *         // Submit payload.nonce to your server for vaulting
 *       });
 *     },
 *
 *     onCancel: function () {
 *       // Handle case where user cancels
 *     },
 *
 *     onError: function (err) {
 *       // Handle case where error occurs
 *     }
 *   });
 *
 *   // Trigger the vault flow when user clicks a button
 *   document.getElementById('save-paypal-button').addEventListener('click', function () {
 *     session.start();
 *   });
 * }).catch(function (err) {
 *  console.error('Error!', err);
 * });
 * ```
 */
function PayPalCheckoutV6(options) {
  this._merchantAccountId = options.merchantAccountId;
}

PayPalCheckoutV6.prototype._initialize = function (options) {
  var config;

  if (options.client) {
    config = options.client.getConfiguration();
    this._authorizationInformation = {
      fingerprint: config.authorizationFingerprint,
      environment: config.gatewayConfiguration.environment,
    };
  } else {
    config = createAuthorizationData(options.authorization);
    this._authorizationInformation = {
      fingerprint: config.attrs.authorizationFingerprint,
      environment: config.environment,
    };
  }

  this._clientPromise = createDeferredClient
    .create({
      authorization: options.authorization,
      client: options.client,
      debug: options.debug,
      assetsUrl: options.authorization
        ? createAssetsUrl.create(options.authorization)
        : undefined,
      name: "PayPal Checkout V6",
    })
    .then(
      function (client) {
        this._configuration = client.getConfiguration();

        if (this._configuration.authorizationType === "TOKENIZATION_KEY") {
          this._setupError = new BraintreeError(
            errors.PAYPAL_CHECKOUT_V6_TOKENIZATION_KEY_NOT_SUPPORTED
          );
        } else if (!this._merchantAccountId) {
          if (!this._configuration.gatewayConfiguration.paypalEnabled) {
            this._setupError = new BraintreeError(
              errors.PAYPAL_CHECKOUT_V6_NOT_ENABLED
            );
          } else if (
            this._configuration.gatewayConfiguration.paypal
              .environmentNoNetwork === true
          ) {
            this._setupError = new BraintreeError(
              errors.PAYPAL_CHECKOUT_V6_SANDBOX_ACCOUNT_NOT_LINKED
            );
          }
        }

        if (this._setupError) {
          return Promise.reject(this._setupError);
        }

        this._client = client;
        analytics.sendEvent(client, constants.ANALYTICS_EVENTS.INITIALIZED);
        this._frameServicePromise = this._setupFrameService(client);

        return client;
      }.bind(this)
    );

  if (options.client) {
    return this._clientPromise.then(
      function () {
        return this;
      }.bind(this)
    );
  }

  return Promise.resolve(this);
};

/**
 * Retrieves the PayPal client ID from the Braintree configuration.
 * @public
 * @example
 * paypalCheckoutV6Instance.getClientId().then(function (clientId) {
 *   console.log('Client ID:', clientId);
 * });
 * @returns {Promise<string>} A promise that resolves with the PayPal client ID.
 */
PayPalCheckoutV6.prototype.getClientId = function () {
  return this._clientPromise.then(function (client) {
    return client.getConfiguration().gatewayConfiguration.paypal.clientId;
  });
};

PayPalCheckoutV6.prototype._setupFrameService = function (client) {
  var frameServicePromise = new ExtendedPromise();
  var config = client.getConfiguration();
  var timeoutRef = setTimeout(function () {
    analytics.sendEvent(client, "paypal-checkout-v6.frame-service.timed-out");
    frameServicePromise.reject(
      new BraintreeError(errors.PAYPAL_CHECKOUT_V6_SESSION_CREATION_FAILED)
    );
  }, INTEGRATION_TIMEOUT_MS);

  this._assetsUrl =
    config.gatewayConfiguration.paypal.assetsUrl + "/web/" + constants.VERSION;
  this._isDebug = config.isDebug;
  this._loadingFrameUrl =
    this._assetsUrl +
    "/html/paypal-landing-frame" +
    useMin(this._isDebug) +
    ".html";

  frameService.create(
    {
      name: "braintreepaypallanding",
      dispatchFrameUrl:
        this._assetsUrl +
        "/html/dispatch-frame" +
        useMin(this._isDebug) +
        ".html",
      openFrameUrl: this._loadingFrameUrl,
    },
    function (service) {
      this._frameService = service;
      clearTimeout(timeoutRef);

      frameServicePromise.resolve();
    }.bind(this)
  );

  return frameServicePromise;
};

/**
 * Checks if the PayPal SDK is loaded and ready for use.
 * @private
 * @returns {boolean} True if PayPal SDK is available.
 */
PayPalCheckoutV6.prototype._isPayPalSdkAvailable = function () {
  return Boolean(window.paypal && window.paypal.createInstance);
};

/**
 * Validates that returnUrl and cancelUrl are provided for app-switch mode.
 * @private
 * @param {object} options Payment session options.
 * @param {string} presentationMode The presentation mode.
 * @returns {BraintreeError|null} Error if validation fails, null otherwise.
 */
PayPalCheckoutV6.prototype._validateAppSwitchUrls = function (
  options,
  presentationMode
) {
  if (
    presentationMode === "direct-app-switch" &&
    (!options.returnUrl || !options.cancelUrl)
  ) {
    return new BraintreeError(
      errors.PAYPAL_CHECKOUT_V6_APP_SWITCH_URLS_REQUIRED
    );
  }

  return null;
};

/**
 * Formats plan metadata for billing agreement creation.
 * @private
 * @param {object} plan - Plan metadata object containing billing cycles and other properties.
 * @returns {object} Formatted plan metadata for the API.
 */
PayPalCheckoutV6.prototype._formatPlanMetadata = function (plan) {
  var planProperties = [
    "currencyIsoCode",
    "name",
    "oneTimeFeeAmount",
    "productDescription",
    "productPrice",
    "productQuantity",
    "shippingAmount",
    "taxAmount",
    "totalAmount",
  ];
  var requiredBillingCycle = [
    "billingFrequency",
    "billingFrequencyUnit",
    "numberOfExecutions",
    "sequence",
    "startDate",
    "trial",
    "pricingScheme",
  ];
  var formattedPlan = { billingCycles: [] };
  var cycleIndex, billingCycle, formattedBillingCycle, cyclePropertyIndex;
  var planPropertyIndex;

  if (plan.hasOwnProperty("billingCycles")) {
    for (cycleIndex = 0; cycleIndex < plan.billingCycles.length; cycleIndex++) {
      billingCycle = plan.billingCycles[cycleIndex];
      formattedBillingCycle = {};

      for (
        cyclePropertyIndex = 0;
        cyclePropertyIndex < requiredBillingCycle.length;
        cyclePropertyIndex++
      ) {
        formattedBillingCycle[requiredBillingCycle[cyclePropertyIndex]] =
          billingCycle[requiredBillingCycle[cyclePropertyIndex]];
      }
      formattedPlan.billingCycles.push(formattedBillingCycle);
    }
  }

  for (
    planPropertyIndex = 0;
    planPropertyIndex < planProperties.length;
    planPropertyIndex++
  ) {
    formattedPlan[planProperties[planPropertyIndex]] =
      plan[planProperties[planPropertyIndex]];
  }

  return formattedPlan;
};

/**
 * Loads the PayPal Web SDK v6 onto the page.
 * @public
 * @param {object} [options] Options for loading the PayPal SDK.
 * @param {string} [options.env] Environment override. Accepts 'stage' (msmaster) or 'teBraintree'.
 * @example
 * paypalCheckoutV6Instance.loadPayPalSDK().then(function () {
 *   // PayPal V6 SDK is now loaded
 * });
 * @example
 * var options = {
 *   env: 'stage'
 * };
 * paypalCheckoutV6Instance.loadPayPalSDK(options).then(function () {
 *   // PayPal V6 SDK is now loaded from the stage environment
 * });
 * @returns {Promise} Returns a promise that resolves when the SDK has been loaded.
 */
PayPalCheckoutV6.prototype.loadPayPalSDK = function (options) {
  var self = this;

  return this._clientPromise.then(function (client) {
    var loadPromise = new ExtendedPromise();
    var config = client.getConfiguration();
    var env = config.gatewayConfiguration.environment;
    var subdomain = env === "production" ? "" : "sandbox.";
    var scriptUrl;

    if (
      options &&
      options.env &&
      constants.PAYPAL_V6_ENVIRONMENT[options.env]
    ) {
      scriptUrl = constants.PAYPAL_V6_ENVIRONMENT[options.env];
    } else {
      scriptUrl = constants.PAYPAL_V6_SDK_BASE_URL.replace("{ENV}", subdomain);
    }

    if (window.paypal && window.paypal.version) {
      analytics.sendEvent(
        client,
        constants.ANALYTICS_EVENTS.SDK_ALREADY_LOADED
      );
      return Promise.resolve(self);
    }

    analytics.sendEvent(client, constants.ANALYTICS_EVENTS.SDK_LOAD_STARTED);

    self._paypalScript = document.createElement("script");
    self._paypalScript.src = scriptUrl;
    self._paypalScript.async = true;

    self._paypalScript.onload = function () {
      analytics.sendEvent(
        client,
        constants.ANALYTICS_EVENTS.SDK_LOAD_SUCCEEDED
      );
      loadPromise.resolve(self);
    };

    self._paypalScript.onerror = function () {
      analytics.sendEvent(client, constants.ANALYTICS_EVENTS.SDK_LOAD_FAILED);
      loadPromise.reject(
        new BraintreeError(errors.PAYPAL_CHECKOUT_V6_SDK_SCRIPT_LOAD_FAILED)
      );
    };

    document.head.appendChild(self._paypalScript);

    return loadPromise;
  });
};

/**
 * Creates a PayPal SDK instance using the Braintree client token.
 * @private
 * @param {object} [options] Options for SDK instance creation.
 * @param {string} [options.flow] Flow type: 'checkout' or 'vault'.
 * @returns {Promise} Resolves with the PayPal SDK instance.
 */
PayPalCheckoutV6.prototype._createPayPalInstance = function (options) {
  var self = this;

  return this._clientPromise.then(function (client) {
    var config = client.getConfiguration();
    var clientId = config.gatewayConfiguration.paypal.clientId;
    var isVaultFlow = options && options.flow === "vault";

    // Use paypal-billing-agreements component for vault flow (Braintree merchants)
    // Use paypal-payments component for one-time payments
    var components = isVaultFlow
      ? ["paypal-billing-agreements"]
      : ["paypal-payments"];

    options = assign(
      {
        clientId: clientId,
        components: components,
        pageType: "checkout",
      },
      options
    );

    if (config.analyticsMetadata && config.analyticsMetadata.sessionId) {
      options.clientMetadataId = config.analyticsMetadata.sessionId;
    }

    analytics.sendEvent(
      client,
      constants.ANALYTICS_EVENTS.CREATE_INSTANCE_STARTED
    );

    return window.paypal
      .createInstance(options)
      .then(function (instance) {
        analytics.sendEvent(
          client,
          constants.ANALYTICS_EVENTS.CREATE_INSTANCE_SUCCEEDED
        );
        self._paypalInstance = instance;

        return instance;
      })
      .catch(function (err) {
        analytics.sendEvent(
          client,
          constants.ANALYTICS_EVENTS.CREATE_INSTANCE_FAILED
        );
        throw convertToBraintreeError(err, {
          type: errors.PAYPAL_CHECKOUT_V6_SDK_INITIALIZATION_FAILED.type,
          code: errors.PAYPAL_CHECKOUT_V6_SDK_INITIALIZATION_FAILED.code,
          message: errors.PAYPAL_CHECKOUT_V6_SDK_INITIALIZATION_FAILED.message,
        });
      });
  });
};

/**
 * @typedef {object} PayPalCheckoutV6~lineItem
 * @property {string} quantity Number of units of the item purchased.
 * @property {string} unitAmount Per-unit price of the item.
 * @property {string} name Item name. Maximum 127 characters.
 * @property {string} kind Indicates whether the line item is a debit (sale) or credit (refund). Accepted values: `debit` and `credit`.
 * @property {?string} unitTaxAmount Per-unit tax price of the item.
 * @property {?string} description Item description. Maximum 127 characters.
 */

/**
 * @typedef {object} PayPalCheckoutV6~shippingOption
 * @property {string} id A unique ID that identifies a shipping option.
 * @property {string} label A description for the shipping option.
 * @property {boolean} selected Whether this option is pre-selected.
 * @property {string} type The shipping type: `SHIPPING` or `PICKUP`.
 * @property {object} amount The shipping cost.
 * @property {string} amount.currency The currency code.
 * @property {string} amount.value The shipping cost amount.
 */

/**
 * @typedef {object} PayPalCheckoutV6~pricingScheme
 * @property {string} pricingModel The pricing model. Options are `FIXED`, `VARIABLE`, or `AUTO_RELOAD`.
 * @property {string} price The price for the billing cycle.
 * @property {string} reloadThresholdAmount The amount at which to reload on auto_reload plans.
 */

/**
 * @typedef {Object} PayPalCheckoutV6~billingCycles
 * @property {(string|number)} billingFrequency The frequency of billing. This value must be a whole number and can't be negative or zero.
 * @property {string} billingFrequencyUnit The unit of billing frequency. Options are `DAY`, `WEEK`, `MONTH`, or `YEAR`.
 * @property {(string|number)} numberOfExecutions The number of executions for the billing cycle.
 * @property {(string|number)} sequence The order in the upcoming billing cycles.
 * @property {string} startDate The start date in ISO 8601 format (`2024-04-06T00:00:00Z`). If populated and the intent is to charge the buyer for the billing cycle at the checkout, it should be populated as current time in ISO 8601 format.
 * @property {boolean} trial Indicates if the billing cycle is a trial.
 * @property {pricingScheme} pricingScheme The {@link PayPalCheckoutV6~pricingScheme|pricing scheme object} for this billing cycle.
 */

/**
 * @typedef {Object} PayPalCheckoutV6~planMetadata
 * @property {billingCycles[]} [billingCycles] An array of {@link PayPalCheckoutV6~billingCycles|billing cycles} for this plan.
 * @property {string} currencyIsoCode The ISO code for the currency, for example `USD`.
 * @property {string} name The name of the plan.
 * @property {(string|number)} oneTimeFeeAmount The one-time fee amount.
 * @property {string} productDescription A description of the product. (Accepts only one element)
 * @property {(string|number)} productPrice The price of the product.
 * @property {(string|number)} productQuantity The quantity of the product. (Accepts only one element)
 * @property {(string|number)} shippingAmount The amount for shipping.
 * @property {(string|number)} taxAmount The amount of tax.
 * @property {string} totalAmount This field is for vault with purchase only. Can include up to 2 decimal places. This value can't be negative or zero.
 */

/**
 * @typedef {object} PayPalCheckoutV6~tokenizePayload
 * @property {string} nonce The payment method nonce.
 * @property {string} type The payment method type, always `PayPalAccount`.
 * @property {object} details Additional PayPal account details.
 * @property {string} details.email User's email address.
 * @property {string} details.payerId User's payer ID, the unique identifier for each PayPal account.
 * @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).
 * @property {?object} details.billingAddress User's billing address details.
 * @property {string} details.billingAddress.line1 Street number and name.
 * @property {string} details.billingAddress.line2 Extended address.
 * @property {string} details.billingAddress.city City or locality.
 * @property {string} details.billingAddress.state State or region.
 * @property {string} details.billingAddress.postalCode Postal code.
 * @property {string} details.billingAddress.countryCode 2 character country code (e.g. US).
 * @property {?object} creditFinancingOffered This property will only be present when the customer pays with PayPal Credit.
 * @property {object} creditFinancingOffered.totalCost This is the estimated total payment amount including interest and fees the user will pay during the lifetime of the loan.
 * @property {string} creditFinancingOffered.totalCost.value An amount defined by ISO 4217 for the given currency.
 * @property {string} creditFinancingOffered.totalCost.currency The currency code for the amount.
 * @property {object} creditFinancingOffered.term This is the estimated amount per month that the customer will need to pay including fees and interest.
 * @property {number} creditFinancingOffered.term.term The number of months that the customer has to pay off this transaction.
 * @property {object} creditFinancingOffered.term.monthlyPayment The estimated amount per month.
 * @property {string} creditFinancingOffered.term.monthlyPayment.value An amount defined by ISO 4217 for the given currency.
 * @property {string} creditFinancingOffered.term.monthlyPayment.currency The currency code for the amount.
 * @property {object} creditFinancingOffered.totalInterest Estimated interest or fees amount the payer will have to pay during the lifetime of the loan.
 * @property {string} creditFinancingOffered.totalInterest.value An amount defined by ISO 4217 for the given currency.
 * @property {string} creditFinancingOffered.totalInterest.currency The currency code for the amount.
 * @property {boolean} creditFinancingOffered.payerAcceptance Status on whether the customer ultimately was approved for and chose to make the payment using the approved installment credit.
 * @property {boolean} creditFinancingOffered.cartAmountImmutable Indicates whether the cart amount is editable after payer's acceptance on PayPal side.
 * @property {?string} shippingOptionId The ID of the selected shipping option.
 * @property {?string} cobrandedCardLabel The label of the co-branded card used.
 */

/**
 * Builds a billing agreement request object from options.
 * @private
 * @param {object} options Billing agreement options.
 * @returns {object} Formatted billing agreement request.
 */
PayPalCheckoutV6.prototype._buildBillingAgreementRequest = function (options) {
  var billingAgreementRequest = {
    planType: options.planType || "UNSCHEDULED",
  };

  if (options.billingAgreementDescription) {
    billingAgreementRequest.description = options.billingAgreementDescription;
  }

  if (options.planMetadata) {
    billingAgreementRequest.planMetadata = this._formatPlanMetadata(
      options.planMetadata
    );
  }

  if (options.amount && options.currency) {
    billingAgreementRequest.amount = options.amount;
    billingAgreementRequest.currency = options.currency;
  }

  if (options.shippingAddressOverride) {
    billingAgreementRequest.shippingAddressOverride =
      options.shippingAddressOverride;
  }

  if (options.userAction) {
    billingAgreementRequest.userAction = options.userAction;
  }

  if (options.offerCredit) {
    billingAgreementRequest.offerCredit = options.offerCredit;
  }

  return billingAgreementRequest;
};

/**
 * Creates a vault setup token via Braintree backend for billing agreements.
 * @private
 * @param {object} options Billing agreement options.
 * @returns {Promise} Resolves with setup token data.
 */
PayPalCheckoutV6.prototype._createBillingAgreementToken = function (options) {
  var self = this;
  var gatewayConfiguration = this._configuration.gatewayConfiguration;

  var payload = {
    returnUrl: options.returnUrl || "https://www.paypal.com/checkoutnow/error",
    cancelUrl: options.cancelUrl || "https://www.paypal.com/checkoutnow/error",
    offerPaypalCredit: options.offerCredit === true,
    merchantAccountId: this._merchantAccountId,
    experienceProfile: {
      brandName: options.displayName || gatewayConfiguration.paypal.displayName,
      noShipping: (!options.enableShippingAddress).toString(),
      addressOverride: false,
    },
  };

  if (options.planType) {
    payload.planType = options.planType;
  }

  if (options.billingAgreementDescription) {
    payload.description = options.billingAgreementDescription;
  }

  if (options.planMetadata) {
    payload.planMetadata = this._formatPlanMetadata(options.planMetadata);
  }

  if (options.amount && options.currency) {
    payload.amount = options.amount;
    payload.currencyIsoCode = options.currency;
  }

  if (options.shippingAddressOverride) {
    payload.shippingAddress = options.shippingAddressOverride;
  }

  if (options.userAction) {
    payload.experienceProfile.userAction = options.userAction;
  }

  return this._clientPromise.then(function (client) {
    analytics.sendEvent(
      client,
      constants.ANALYTICS_EVENTS.CREATE_BA_TOKEN_STARTED
    );

    return client
      .request({
        endpoint: "paypal_hermes/setup_billing_agreement",
        method: "post",
        data: payload,
      })
      .then(function (response) {
        analytics.sendEvent(
          client,
          constants.ANALYTICS_EVENTS.CREATE_BA_TOKEN_SUCCEEDED
        );

        var approvalTokenId = response.agreementSetup.tokenId;

        if (!approvalTokenId) {
          throw new Error(
            "Could not extract approval token from billing agreement setup"
          );
        }

        return {
          approvalTokenId: approvalTokenId,
          agreementSetup: response.agreementSetup,
        };
      })
      .catch(function (err) {
        analytics.sendEvent(
          client,
          constants.ANALYTICS_EVENTS.CREATE_BA_TOKEN_FAILED
        );

        if (self._setupError) {
          throw self._setupError;
        }

        throw convertToBraintreeError(err, {
          type: errors.PAYPAL_CHECKOUT_V6_BILLING_AGREEMENT_CREATION_FAILED
            .type,
          code: errors.PAYPAL_CHECKOUT_V6_BILLING_AGREEMENT_CREATION_FAILED
            .code,
          message:
            errors.PAYPAL_CHECKOUT_V6_BILLING_AGREEMENT_CREATION_FAILED.message,
        });
      });
  });
};

/**
 * Creates a PayPal order via Braintree backend.
 * @private
 * @param {object} options Payment options.
 * @param {object} [config] Optional configuration overrides (returnUrl, cancelUrl).
 * @returns {Promise} Resolves with order data including orderId.
 */
PayPalCheckoutV6.prototype._createPaymentResource = function (options) {
  var self = this;
  var gatewayConfiguration = this._configuration.gatewayConfiguration;
  var intent = options.intent || "capture";

  if (intent === "capture") {
    intent = "sale";
  }

  var payload = {
    amount: options.amount,
    currencyIsoCode: options.currency,
    intent: intent,
    returnUrl: options.returnUrl || "https://www.paypal.com/checkoutnow/error",
    cancelUrl: options.cancelUrl || "https://www.paypal.com/checkoutnow/error",
    experienceProfile: {
      brandName: options.displayName || gatewayConfiguration.paypal.displayName,
    },
  };

  if (this._merchantAccountId) {
    payload.merchantAccountId = this._merchantAccountId;
  }

  if (options.lineItems) {
    payload.lineItems = options.lineItems;
  }

  if (options.shippingOptions) {
    payload.shippingOptions = options.shippingOptions;
  }

  if (options.userAuthenticationEmail) {
    payload.payer_email = options.userAuthenticationEmail; // eslint-disable-line camelcase
  }

  if (options.amountBreakdown) {
    payload.amountBreakdown = options.amountBreakdown;
  }

  if (options.offerCredit === true) {
    payload.offerPaypalCredit = true;
  }

  return this._clientPromise.then(function (client) {
    analytics.sendEvent(
      client,
      constants.ANALYTICS_EVENTS.CREATE_ORDER_STARTED
    );

    return client
      .request({
        endpoint: "paypal_hermes/create_payment_resource",
        method: "post",
        data: payload,
      })
      .then(function (response) {
        analytics.sendEvent(
          client,
          constants.ANALYTICS_EVENTS.CREATE_ORDER_SUCCEEDED
        );

        // Extract order ID from response
        var redirectUrl = response.paymentResource.redirectUrl;
        var match = redirectUrl.match(/token=([^&]+)/);
        var orderId = match ? match[1] : null;

        if (!orderId) {
          throw new Error("Could not extract order ID from payment resource");
        }

        self._contextId = orderId;

        return {
          orderId: orderId,
          paymentResource: response.paymentResource,
        };
      })
      .catch(function (err) {
        analytics.sendEvent(
          client,
          constants.ANALYTICS_EVENTS.CREATE_ORDER_FAILED
        );

        if (self._setupError) {
          throw self._setupError;
        }

        throw convertToBraintreeError(err, {
          type: errors.PAYPAL_CHECKOUT_V6_ORDER_CREATION_FAILED.type,
          code: errors.PAYPAL_CHECKOUT_V6_ORDER_CREATION_FAILED.code,
          message: errors.PAYPAL_CHECKOUT_V6_ORDER_CREATION_FAILED.message,
        });
      });
  });
};

/**
 * Creates session callback handlers for checkout flow with analytics.
 * @private
 * @param {object} client The Braintree client instance.
 * @param {object} options Payment session options containing callbacks.
 * @returns {object} Callback configuration for PayPal session.
 */
PayPalCheckoutV6.prototype._createCheckoutSessionCallbacks = function (
  client,
  options
) {
  var sessionCallbacks = {
    onApprove: function (data) {
      analytics.sendEvent(client, constants.ANALYTICS_EVENTS.PAYMENT_APPROVED);

      return options.onApprove(data);
    },
    onCancel: function (data) {
      analytics.sendEvent(client, constants.ANALYTICS_EVENTS.PAYMENT_CANCELED);
      if (options && typeof options.onCancel === "function") {
        return options.onCancel(data);
      }

      return undefined;
    },
  };

  if (options && typeof options.onError === "function") {
    sessionCallbacks.onError = function (err) {
      return options.onError(err);
    };
  }

  if (options && typeof options.onShippingAddressChange === "function") {
    sessionCallbacks.onShippingAddressChange = function (data) {
      return options.onShippingAddressChange(data);
    };
  }

  return sessionCallbacks;
};

/**
 * Creates order promise for checkout flow with error handling.
 * @private
 * @param {object} paymentOptions Payment resource options.
 * @param {function} [onError] Optional error callback.
 * @returns {Promise} Promise resolving to { orderId }.
 */
PayPalCheckoutV6.prototype._createOrderPromise = function (
  paymentOptions,
  onError
) {
  return this._createPaymentResource(paymentOptions)
    .then(function (orderData) {
      return { orderId: orderData.orderId };
    })
    .catch(function (err) {
      if (onError) {
        onError(err);
      }
      throw err;
    });
};

/**
 * Starts a checkout payment session with the given instance and client.
 * @private
 * @param {object} instance PayPal SDK instance.
 * @param {object} client Braintree client instance.
 * @param {string} presentationMode How to present the PayPal flow.
 * @param {string} sessionType Type of session: 'paypal' or 'paypal-credit'.
 * @param {object} options Payment session options.
 * @param {object} paymentOptions Payment resource options.
 * @returns {Promise} Promise from PayPal session.start().
 */
PayPalCheckoutV6.prototype._startCheckoutSession = function (
  instance,
  client,
  presentationMode,
  sessionType,
  options,
  paymentOptions
) {
  var sessionMethod =
    sessionType === "paypal-credit"
      ? "createPayPalCreditOneTimePaymentSession"
      : "createPayPalOneTimePaymentSession";

  analytics.sendEvent(client, constants.ANALYTICS_EVENTS.PAYMENT_STARTED);

  var session = instance[sessionMethod](
    this._createCheckoutSessionCallbacks(client, options)
  );

  return session.start(
    {
      presentationMode: presentationMode,
    },
    this._createOrderPromise(paymentOptions, options.onError)
  );
};

/**
 * Creates session callback handlers for billing agreement flow with analytics.
 * @private
 * @param {object} client The Braintree client instance.
 * @param {object} options Billing agreement session options containing callbacks.
 * @returns {object} Callback configuration for PayPal session.
 */
PayPalCheckoutV6.prototype._createBillingSessionCallbacks = function (
  client,
  options
) {
  return {
    onApprove: function (data) {
      analytics.sendEvent(
        client,
        constants.ANALYTICS_EVENTS.CREATE_BA_SESSION_APPROVED
      );

      return options.onApprove(data);
    },
    onCancel: function (data) {
      analytics.sendEvent(
        client,
        constants.ANALYTICS_EVENTS.CREATE_BA_SESSION_CANCELED
      );
      if (options.onCancel && typeof options.onCancel === "function") {
        options.onCancel(data);
      }
    },
    onError: function (err) {
      analytics.sendEvent(
        client,
        constants.ANALYTICS_EVENTS.CREATE_BA_SESSION_FAILED
      );
      if (options.onError && typeof options.onError === "function") {
        options.onError(err);
      }
    },
  };
};

/**
 * Creates billing token promise for vault flow.
 * @private
 * @param {object} billingAgreementRequest Billing agreement request data.
 * @param {object} options Billing agreement session options.
 * @returns {Promise} Promise resolving to { billingToken }.
 */
PayPalCheckoutV6.prototype._createBillingTokenPromise = function (
  billingAgreementRequest,
  options
) {
  return this.createPayment({
    flow: "vault",
    billingAgreementDescription: billingAgreementRequest.description,
    planType: billingAgreementRequest.planType,
    planMetadata: billingAgreementRequest.planMetadata,
    amount: billingAgreementRequest.amount,
    currency: billingAgreementRequest.currency,
    shippingAddressOverride: billingAgreementRequest.shippingAddressOverride,
    userAction: billingAgreementRequest.userAction,
    offerCredit: options.offerCredit,
  }).then(function (billingToken) {
    return { billingToken: billingToken };
  });
};

/**
 * Starts a billing agreement session with the given instance and client.
 * @private
 * @param {object} instance PayPal SDK instance.
 * @param {object} client Braintree client instance.
 * @param {string} presentationMode How to present the PayPal flow.
 * @param {object} billingAgreementRequest Billing agreement request data.
 * @param {object} options Billing agreement session options.
 * @returns {Promise} Promise from PayPal session.start().
 */
PayPalCheckoutV6.prototype._startBillingSession = function (
  instance,
  client,
  presentationMode,
  billingAgreementRequest,
  options
) {
  var self = this;

  analytics.sendEvent(
    client,
    constants.ANALYTICS_EVENTS.CREATE_BA_SESSION_STARTED
  );

  var paypalSession = instance.createPayPalBillingAgreementWithoutPurchase(
    this._createBillingSessionCallbacks(client, options)
  );

  analytics.sendEvent(
    client,
    constants.ANALYTICS_EVENTS.CREATE_BA_SESSION_SESSION_CREATED
  );

  var billingTokenPromise = self
    ._createBillingTokenPromise(billingAgreementRequest, options)
    .catch(function (err) {
      if (options.onError) {
        options.onError(err);
      }
      throw err;
    });

  return paypalSession.start(
    { presentationMode: presentationMode },
    billingTokenPromise
  );
};

/**
 * @private
 * @param {object} options Payment session options.
 * @param {string} sessionType Type of session: 'paypal' or 'paypal-credit'.
 * @returns {object} Payment session object with `start()` method.
 */
PayPalCheckoutV6.prototype._createPaymentSession = function (
  options,
  sessionType
) {
  var self = this;

  this._flow = "checkout";
  this._sessionType = sessionType;

  var paymentOptions = {
    amount: options.amount,
    currency: options.currency,
    intent: options.intent || "capture",
  };

  if (options.offerCredit) {
    paymentOptions.offerCredit = options.offerCredit;
  }

  if (options.lineItems) {
    paymentOptions.lineItems = options.lineItems;
  }

  if (options.shippingOptions) {
    paymentOptions.shippingOptions = options.shippingOptions;
  }

  if (options.userAuthenticationEmail) {
    paymentOptions.userAuthenticationEmail = options.userAuthenticationEmail;
  }

  if (options.amountBreakdown) {
    paymentOptions.amountBreakdown = options.amountBreakdown;
  }

  if (options.returnUrl) {
    paymentOptions.returnUrl = options.returnUrl;
  }

  if (options.cancelUrl) {
    paymentOptions.cancelUrl = options.cancelUrl;
  }

  return {
    /**
     * Starts the PayPal payment flow.
     * @param {object} [presentationOptions] Options for how to present PayPal.
     * @param {string} [presentationOptions.presentationMode='auto'] Presentation mode.
     * @returns {Promise} Resolves when payment flow completes.
     */
    start: function (presentationOptions) {
      presentationOptions = presentationOptions || {};
      var presentationMode =
        presentationOptions.presentationMode ||
        options.presentationMode ||
        "auto";
      var paypalInstance = self._paypalInstance;

      var appSwitchError = self._validateAppSwitchUrls(
        options,
        presentationMode
      );

      if (appSwitchError) {
        return Promise.reject(appSwitchError);
      }

      // If the PayPal instance is ready, start synchronously to preserve Safari's transient activation
      if (paypalInstance && self._client) {
        return self._startCheckoutSession(
          paypalInstance,
          self._client,
          presentationMode,
          sessionType,
          options,
          paymentOptions
        );
      }

      // If instance isn't ready yet, wait for it. This path may _not_ work in Safari
      // due to transient activation expiring, but works in Chrome/Firefox/Edge
      if (self._checkoutInstancePromise) {
        return self._checkoutInstancePromise
          .then(function (instance) {
            return self._clientPromise.then(function (client) {
              return self._startCheckoutSession(
                instance,
                client,
                presentationMode,
                sessionType,
                options,
                paymentOptions
              );
            });
          })
          .catch(function (err) {
            if (options.onError) {
              options.onError(err);
            }
            throw err;
          });
      }

      // No instance and no promise - this shouldn't happen if loadPayPalSDK() was called
      return Promise.reject(
        new BraintreeError(errors.PAYPAL_CHECKOUT_V6_INSTANCE_NOT_READY)
      );
    },
  };
};

/**
 * Creates a one-time PayPal payment session.
 * @public
 * @param {object} options Payment session options.
 * @param {string} options.amount The payment amount (e.g., '10.00').
 * @param {string} options.currency The currency code (e.g., 'USD').
 * @param {string} [options.intent='capture'] Payment intent: 'authorize', 'capture', or 'order'.
 * @param {boolean} [options.offerCredit=false] Offers PayPal Credit as the default funding instrument for the transaction. If the customer isn't pre-approved for PayPal Credit, they will be prompted to apply for it.
 * @param {function} options.onApprove Called when the customer approves the payment.
 * @param {function} [options.onCancel] Called when the customer cancels the payment.
 * @param {function} [options.onError] Called when an error occurs.
 * @param {function} [options.onShippingAddressChange] Called when the customer changes their shipping address. Return a Promise to update the payment details.
 * @param {lineItem[]} [options.lineItems] Line items for this transaction.
 * @param {shippingOption[]} [options.shippingOptions] Shipping options.
 * @param {string} [options.userAuthenticationEmail] Pre-fill the PayPal login email.
 * @param {object} [options.amountBreakdown] Breakdown of the amount.
 * @param {string} [options.returnUrl] URL to return to after payment completion. This parameter is required when using app switch presentation mode; for other flows, it is optional and defaults to the PayPal error page if not provided.
 * @param {string} [options.cancelUrl] URL to return to after payment cancellation. This parameter is required when using app switch presentation mode; for other flows, it is optional and defaults to the PayPal error page if not provided.
 * @param {string} [options.displayName] The merchant name displayed inside of the PayPal lightbox; defaults to the company name on your Braintree account.
 * @param {string} [options.presentationMode='auto'] How to present PayPal: 'auto', 'popup', 'modal', 'redirect', 'payment-handler'.
 * @example
 * // Standard PayPal payment
 * var session = paypalCheckoutV6Instance.createOneTimePaymentSession({
 *   amount: '10.00',
 *   currency: 'USD',
 *   intent: 'capture',
 *   onApprove: function (data) {
 *     return paypalCheckoutV6Instance.tokenizePayment(data).then(function (payload) {
 *       // Send payload.nonce to your server
 *     });
 *   },
 *   onCancel: function () {
 *     console.log('Payment canceled');
 *   },
 *   onError: function (err) {
 *     console.error('Payment error:', err);
 *   }
 * });
 *
 * @example
 * // PayPal payment with direct app switch
 * async function initializePayPal() {
 *   try {
 *     // Create client and PayPal Checkout v6 instances
 *     const clientInstance = await braintree.client.create({
 *       authorization: 'your_client_token'
 *     });
 *
 *     const paypalCheckoutV6Instance = await braintree.paypalCheckoutV6.create({
 *       client: clientInstance
 *     });
 *
 *     // Load the PayPal SDK
 *     await paypalCheckoutV6Instance.loadPayPalSDK();
 *
 *     // Create a one-time payment session
 *     const session = paypalCheckoutV6Instance.createOneTimePaymentSession({
 *       amount: '10.00',
 *       currency: 'USD',
 *       intent: 'capture',
 *       // When using app switch, returnUrl and cancelUrl must be specified
 *       // These should be URLs in your application that can handle the return flow
 *       returnUrl: 'https://example.com/return',
 *       cancelUrl: 'https://example.com/cancel',
 *       onApprove: function (data) {
 *         return paypalCheckoutV6Instance.tokenizePayment(data).then(function (payload) {
 *           // Send payload.nonce to your server
 *           console.log('Payment approved, nonce:', payload.nonce);
 *         });
 *       },
 *       onCancel: function () {
 *         console.log('Payment canceled');
 *       },
 *       onError: function (err) {
 *         console.error('Payment error:', err);
 *       }
 *     });
 *
 *     // First, check if returning from app switch using the hasReturned() method
 *     // This should be done when your page loads, before showing any UI
 *     if (session.hasReturned()) {
 *       // Resume the payment session to complete the flow
 *       await session.resume();
 *     } else {
 *       // Initial flow - set up button click handler
 *       document.getElementById('paypal-button').addEventListener('click', async () => {
 *         try {
 *           // Start the payment with app switch configuration
 *           const { redirectURL } = await session.start({
 *             // Use direct-app-switch mode to enable app switch flow
 *             presentationMode: 'direct-app-switch',
 *             autoRedirect: {
 *               enabled: true
 *             }
 *           });
 *
 *           // If autoRedirect is disabled or fails, manually redirect
 *           if (redirectURL) {
 *             window.location.assign(redirectURL);
 *           }
 *         } catch (error) {
 *           console.error('Error starting payment:', error);
 *         }
 *       });
 *     }
 *   } catch (error) {
 *     console.error('Initialization error:', error);
 *   }
 * }
 *
 * // Initialize on page load
 * initializePayPal();
 *
 * @example
 * // PayPal Credit payment
 * var session = paypalCheckoutV6Instance.createOneTimePaymentSession({
 *   amount: '10.00',
 *   currency: 'USD',
 *   offerCredit: true, // Offer PayPal Credit
 *   onApprove: function (data) {
 *     return paypalCheckoutV6Instance.tokenizePayment(data).then(function (payload) {
 *       // payload will include creditFinancingOffered if customer used PayPal Credit
 *       // Send payload.nonce to your server
 *     });
 *   }
 * });
 *
 * @example
 * // Dynamic shipping price calculation with onShippingAddressChange
 * var session = paypalCheckoutV6Instance.createOneTimePaymentSession({
 *   amount: '10.00',
 *   currency: 'USD',
 *   lineItems: [
 *     {
 *       quantity: '1',
 *       unitAmount: '10.00',
 *       name: 'Product Name',
 *       kind: 'debit'
 *     }
 *   ],
 *   shippingOptions: [
 *     {
 *       id: 'economy',
 *       label: 'Economy Shipping (5-7 days)',
 *       selected: true,
 *       type: 'SHIPPING',
 *       amount: {
 *         currency: 'USD',
 *         value: '0.00'
 *       }
 *     },
 *     {
 *       id: 'express',
 *       label: 'Express Shipping (2-3 days)',
 *       selected: false,
 *       type: 'SHIPPING',
 *       amount: {
 *         currency: 'USD',
 *         value: '5.00'
 *       }
 *     }
 *   ],
 *   amountBreakdown: {
 *     itemTotal: '10.00',
 *     shipping: '0.00'
 *   },
 *   onShippingAddressChange: function (data, actions) {
 *     // Calculate shipping cost based on selected option and shipping address
 *     var newShippingCost = '0.00';
 *     var newTotal = '10.00';
 *
 *     // Example: determine shipping cost based on selected option
 *     if (data.selectedShippingOption) {
 *       if (data.selectedShippingOption.id === 'express') {
 *         newShippingCost = '5.00';
 *         newTotal = '15.00';
 *       }
 *     }
 *
 *     // Example: determine shipping cost based on country
 *     if (data.shippingAddress && data.shippingAddress.countryCode) {
 *       if (data.shippingAddress.countryCode === 'CA') {
 *         newShippingCost = '7.50';
 *         newTotal = '17.50';
 *       }
 *     }
 *
 *     // Update the payment with new amount and breakdown
 *     return paypalCheckoutV6Instance.updatePayment({
 *       paymentId: data.orderId,
 *       amount: newTotal,
 *       currency: 'USD',
 *       shippingOptions: [
 *         {
 *           id: 'economy',
 *           label: 'Economy Shipping (5-7 days)',
 *           selected: data.selectedShippingOption && data.selectedShippingOption.id === 'economy',
 *           type: 'SHIPPING',
 *           amount: {
 *             currency: 'USD',
 *             value: '0.00'
 *           }
 *         },
 *         {
 *           id: 'express',
 *           label: 'Express Shipping (2-3 days)',
 *           selected: data.selectedShippingOption && data.selectedShippingOption.id === 'express',
 *           type: 'SHIPPING',
 *           amount: {
 *             currency: 'USD',
 *             value: '5.00'
 *           }
 *         }
 *       ],
 *       amountBreakdown: {
 *         itemTotal: '10.00',
 *         shipping: newShippingCost
 *       }
 *     });
 *   },
 *   onApprove: function (data) {
 *     return paypalCheckoutV6Instance.tokenizePayment(data);
 *   }
 * });
 *
 * // Later, trigger the payment flow
 * button.addEventListener('click', function () {
 *   session.start();
 * });
 *
 * @returns {object} Payment session object with `start()` method.
 */
PayPalCheckoutV6.prototype.createOneTimePaymentSession = function (options) {
  var self = this;

  // Validate required options
  if (!options || !options.amount || !options.currency || !options.onApprove) {
    throw new BraintreeError(errors.PAYPAL_CHECKOUT_V6_INVALID_SESSION_OPTIONS);
  }

  analytics.sendEvent(
    self._clientPromise,
    constants.ANALYTICS_EVENTS.SESSION_CHECKOUT_CREATED
  );

  var sessionType = options.offerCredit === true ? "paypal-credit" : "paypal";

  if (options.offerCredit === true) {
    analytics.sendEvent(
      self._clientPromise,
      constants.ANALYTICS_EVENTS.CREDIT_OFFERED
    );
  }

  // Eagerly create PayPal instance so start() can run synchronously (required for Safari)
  if (
    !self._paypalInstance &&
    !self._checkoutInstancePromise &&
    self._isPayPalSdkAvailable()
  ) {
    self._checkoutInstancePromise = self._clientPromise
      .then(function () {
        return self._createPayPalInstance();
      })
      .then(function (instance) {
        // _paypalInstance is set by _createPayPalInstance, but we also need the promise
        return instance;
      });
  }

  return this._createPaymentSession(options, sessionType);
};

/**
 * Creates a payment or billing agreement.
 * @public
 * @param {object} options Payment options.
 * @param {string} options.flow 'checkout' for one-time payment or 'vault' for billing agreement.
 * @param {string} [options.amount] Amount (required for checkout flow).
 * @param {string} [options.currency] Currency code (required for checkout flow).
 * @param {string} [options.intent] Payment intent: 'authorize', 'capture', or 'order'.
 * @param {boolean} [options.offerCredit] Whether to offer PayPal Credit.
 * @param {string} [options.billingAgreementDescription] Description for vault flow.
 * @param {string} [options.planType] Plan type for vault flow.
 * @param {object} [options.planMetadata] Plan metadata for vault flow.
 * @param {object} [options.shippingAddressOverride] Shipping address override.
 * @param {string} [options.userAction] User action (CONTINUE, COMMIT, SETUP_NOW).
 * @param {string} [options.displayName] The merchant name displayed inside of the PayPal lightbox; defaults to the company name on your Braintree account.
 * @example
 * // Basic checkout flow
 * paypalCheckoutV6Instance.createPayment({
 *   flow: 'checkout',
 *   amount: '10.00',
 *   currency: 'USD',
 *   intent: 'capture'
 * }).then(function(orderId) {
 *   console.log('Order ID:', orderId);
 * });
 *
 * @example
 * // Basic vault flow
 * paypalCheckoutV6Instance.createPayment({
 *   flow: 'vault'
 * }).then(function(billingToken) {
 *   console.log('BA Token:', billingToken);
 * });
 *
 * @example <caption>use the new plan features</caption>
 * // Plan and plan metadata are passed to createPayment
 * paypalCheckoutV6Instance.createPayment({
 *   flow: 'vault',
 *   planType: 'RECURRING',
 *   planMetadata: {
 *     billingCycles: [
 *       {
 *          billingFrequency: "1",
 *          billingFrequencyUnit: "MONTH",
 *          numberOfExecutions: "1",
 *          sequence: "1",
 *          startDate: "2024-04-06T00:00:00Z",
 *          trial: true,
 *          pricingScheme: {
 *            pricingModel: "FIXED",
 *        },
 *      },
 *      ],
 *     currencyIsoCode: "USD",
 *     name: "Netflix with Ads",
 *     productDescription: "iPhone 13",
 *     productQuantity: "1.0",
 *     oneTimeFeeAmount: "10",
 *     shippingAmount: "3.0",
 *     productPrice: "200",
 *     taxAmount: "20",
 *  };
 * });
 *
 * @example <caption>Vault Flow with Purchase (Billing Agreement with Recurring Payment)</caption>
 * paypalCheckoutV6Instance.createPayment({
 *   flow: 'vault',
 *   amount: '12.50',
 *   currency: 'USD',
 *   planType: 'SUBSCRIPTION',
 *   planMetadata: {
 *     billingCycles: [{
 *       billingFrequency: 1,
 *       billingFrequencyUnit: "MONTH",
 *       sequence: 1,
 *       pricingScheme: {
 *         pricingModel: "FIXED",
 *         price: "10.00"
 *       },
 *     }],
 *     currencyIsoCode: "USD",
 *     totalAmount: "10.00",
 *     name: "My Recurring Product"
 *   }
 * });
 *
 * @returns {Promise<string>} BA token for vault flow or order ID for checkout flow.
 */
PayPalCheckoutV6.prototype.createPayment = function (options) {
  var self = this;

  return new Promise(function (resolve, reject) {
    var billingAgreementRequest;

    if (!options || !options.flow) {
      reject(
        new BraintreeError(errors.PAYPAL_CHECKOUT_V6_INVALID_SESSION_OPTIONS)
      );

      return;
    }

    self._flow = options.flow;

    if (options.flow === "vault") {
      billingAgreementRequest = self._buildBillingAgreementRequest(options);

      self
        ._createBillingAgreementToken(billingAgreementRequest)
        .then(function (tokenData) {
          resolve(tokenData.approvalTokenId);
        })
        .catch(reject);

      return;
    }

    self
      ._createPaymentResource(options)
      .then(function (orderData) {
        resolve(orderData.orderId);
      })
      .catch(reject);
  });
};

/**
 * Creates a billing agreement session for vault flow.
 * @public
 * @param {object} options Options for creating a billing agreement session.
 * @param {string} [options.billingAgreementDescription] Description for the billing agreement.
 * @param {string} [options.planType] Type of plan: RECURRING, SUBSCRIPTION, UNSCHEDULED, or INSTALLMENTS.
 * @param {object} [options.planMetadata] Metadata about the plan including billing cycles.
 * @param {array} [options.planMetadata.billingCycles] Array of billing cycle objects.
 * @param {string} [options.planMetadata.currencyIsoCode] Currency ISO code for the plan.
 * @param {string} [options.planMetadata.name] Name of the plan.
 * @param {string} [options.planMetadata.productDescription] Description of the product.
 * @param {string} [options.amount] Amount for vault with purchase flow.
 * @param {string} [options.currency] Currency for vault with purchase flow.
 * @param {boolean} [options.offerCredit=false] Whether to offer PayPal Credit.
 * @param {object} [options.shippingAddressOverride] Shipping address to override.
 * @param {string} [options.userAction] User action: CONTINUE, COMMIT, or SETUP_NOW.
 * @param {string} [options.displayName] The merchant name displayed inside of the PayPal lightbox; defaults to the company name on your Braintree account.
 * @param {string} [options.returnUrl] URL to return to after billing agreement approval. This parameter is required when using app switch presentation mode; for other flows, it is optional and defaults to the PayPal error page if not provided.
 * @param {string} [options.cancelUrl] URL to return to after billing agreement cancellation. This parameter is required when using app switch presentation mode; for other flows, it is optional and defaults to the PayPal error page if not provided.
 * @param {function} options.onApprove Callback when customer approves the billing agreement.
 * @param {function} [options.onCancel] Callback when customer cancels.
 * @param {function} [options.onError] Callback when an error occurs.
 * @param {string} [options.presentationMode] Presentation mode: auto, popup, modal, redirect, or payment-handler.
 * @example
 * var session = paypalCheckoutV6Instance.createBillingAgreementSession({
 *   billingAgreementDescription: 'Monthly subscription for premium service',
 *   planType: 'SUBSCRIPTION',
 *   planMetadata: {
 *     currencyIsoCode: 'USD',
 *     name: 'Premium Subscription',
 *     productDescription: 'Monthly premium access',
 *     billingCycles: [{
 *       billingFrequency: 1,
 *       billingFrequencyUnit: 'MONTH',
 *       numberOfExecutions: 0,
 *       sequence: 1,
 *       startDate: '2025-12-01T00:00:00Z',
 *       trial: false,
 *       pricingScheme: {
 *         pricingModel: 'FIXED',
 *         price: '9.99'
 *       }
 *     }]
 *   },
 *   onApprove: function (data) {
 *     console.log('Billing agreement approved:', data);
 *   },
 *   onCancel: function () {
 *     console.log('Billing agreement canceled');
 *   },
 *   onError: function (err) {
 *     console.error('Billing agreement error:', err);
 *   }
 * });
 *
 * // Trigger the vault flow when user clicks a button
 * document.getElementById('paypal-button').addEventListener('click', function () {
 *   session.start();
 * });
 * @returns {object} Session object with start() method.
 */
PayPalCheckoutV6.prototype.createBillingAgreementSession = function (options) {
  var self = this;
  var session = {};

  if (!options) {
    throw new BraintreeError(
      errors.PAYPAL_CHECKOUT_V6_INVALID_BILLING_AGREEMENT_OPTIONS
    );
  }

  if (!options.onApprove || typeof options.onApprove !== "function") {
    throw new BraintreeError(
      errors.PAYPAL_CHECKOUT_V6_INVALID_BILLING_AGREEMENT_OPTIONS
    );
  }

  analytics.sendEvent(
    self._clientPromise,
    constants.ANALYTICS_EVENTS.SESSION_VAULT_CREATED
  );

  // Eagerly create PayPal instance so start() can run synchronously (required for Safari).
  if (
    !self._paypalVaultInstance &&
    !self._vaultInstancePromise &&
    self._isPayPalSdkAvailable()
  ) {
    self._vaultInstancePromise = self._clientPromise
      .then(function () {
        return self._createPayPalInstance({ flow: "vault" });
      })
      .then(function (instance) {
        self._paypalVaultInstance = instance;

        return instance;
      });
  }

  var billingAgreementRequest = self._buildBillingAgreementRequest(options);

  session.start = function (presentationOptions) {
    presentationOptions = presentationOptions || {};
    var presentationMode =
      presentationOptions.presentationMode ||
      options.presentationMode ||
      "auto";
    var paypalInstance = self._paypalVaultInstance;

    var appSwitchError = self._validateAppSwitchUrls(options, presentationMode);

    if (appSwitchError) {
      return Promise.reject(appSwitchError);
    }

    // If the PayPal vault instance is ready,
    // start synchronously to preserve Safari's transient activation
    if (paypalInstance && self._client) {
      return self._startBillingSession(
        paypalInstance,
        self._client,
        presentationMode,
        billingAgreementRequest,
        options
      );
    }

    // If instance isn't ready yet, wait for it. This path may _not_ work in Safari
    // due to transient activation expiring, but works in Chrome/Firefox/Edge
    if (self._vaultInstancePromise) {
      return self._vaultInstancePromise
        .then(function (instance) {
          return self._clientPromise.then(function (client) {
            if (!self._isPayPalSdkAvailable()) {
              analytics.sendEvent(
                client,
                constants.ANALYTICS_EVENTS.CREATE_BA_SESSION_SDK_NOT_LOADED
              );
              throw new BraintreeError(
                errors.PAYPAL_CHECKOUT_V6_SDK_INITIALIZATION_FAILED
              );
            }

            analytics.sendEvent(
              client,
              constants.ANALYTICS_EVENTS.CREATE_BA_SESSION_INSTANCE_CREATED
            );

            return self._startBillingSession(
              instance,
              client,
              presentationMode,
              billingAgreementRequest,
              options
            );
          });
        })
        .catch(function (err) {
          return self._clientPromise.then(function (client) {
            analytics.sendEvent(
              client,
              constants.ANALYTICS_EVENTS.CREATE_BA_SESSION_FAILED
            );

            throw new BraintreeError({
              type: errors.PAYPAL_CHECKOUT_V6_BILLING_AGREEMENT_CREATION_FAILED
                .type,
              code: errors.PAYPAL_CHECKOUT_V6_BILLING_AGREEMENT_CREATION_FAILED
                .code,
              message:
                errors.PAYPAL_CHECKOUT_V6_BILLING_AGREEMENT_CREATION_FAILED
                  .message,
              details: { originalError: err },
            });
          });
        });
    }

    // No instance and no promise. Check if SDK wasn't loaded
    if (!self._isPayPalSdkAvailable()) {
      return self._clientPromise.then(function (client) {
        analytics.sendEvent(
          client,
          constants.ANALYTICS_EVENTS.CREATE_BA_SESSION_SDK_NOT_LOADED
        );
        throw new BraintreeError(
          errors.PAYPAL_CHECKOUT_V6_SDK_INITIALIZATION_FAILED
        );
      });
    }

    // Otherwise, this shouldn't happen if loadPayPalSDK() was called properly
    return Promise.reject(
      new BraintreeError(errors.PAYPAL_CHECKOUT_V6_INSTANCE_NOT_READY)
    );
  };

  return session;
};

/**
 * Tokenizes a PayPal payment or billing agreement.
 * @public
 * @param {object} options Options for tokenizing the payment.
 * @param {string} [options.payerID] Payer ID returned by PayPal for one-time payments.
 * @param {string} [options.orderID] Order ID returned by PayPal for one-time payments.
 * @param {string} [options.billingToken] Billing token returned by PayPal for vault flow.
 * @param {boolean} [options.vault=true] Whether or not to vault the resulting PayPal account (if using a client token generated with a customer id and the vault flow).
 * @example
 * // One-time payment tokenization
 * paypalCheckoutV6Instance.tokenizePayment({
 *   payerID: data.payerID,
 *   orderID: data.orderID
 * }).then(function (payload) {
 *   console.log('Payment method nonce:', payload.nonce);
 * });
 *
 * @example <caption>Vault flow tokenization</caption>
 * paypalCheckoutV6Instance.tokenizePayment({
 *   billingToken: data.billingToken
 * }).then(function (payload) {
 *   console.log('Vaulted payment method nonce:', payload.nonce);
 * });
 *
 * @example <caption>Opt out of auto-vaulting behavior</caption>
 * paypalCheckoutV6Instance.tokenizePayment({
 *   billingToken: data.billingToken,
 *   vault: false
 * }).then(function (payload) {
 *   console.log('Payment method nonce (not vaulted):', payload.nonce);
 * });
 *
 * @returns {Promise} A promise that resolves with the tokenization payload.
 */
PayPalCheckoutV6.prototype.tokenizePayment = function (options) {
  var self = this;

  return new Promise(function (resolve, reject) {
    var shouldVault = true;
    var isBillingAgreement = Boolean(options && options.billingToken);

    if (!options) {
      reject(
        new BraintreeError(errors.PAYPAL_CHECKOUT_V6_MISSING_TOKENIZATION_DATA)
      );

      return;
    }

    // Validate required parameters
    if (!isBillingAgreement && (!options.payerID || !options.orderID)) {
      reject(
        new BraintreeError(errors.PAYPAL_CHECKOUT_V6_MISSING_TOKENIZATION_DATA)
      );

      return;
    }

    if (options.hasOwnProperty("vault")) {
      shouldVault = options.vault;
    }

    self._clientPromise.then(function (client) {
      var endpoint, data, analyticsPrefix;

      if (isBillingAgreement) {
        analyticsPrefix = constants.ANALYTICS_EVENTS.TOKENIZE_BA_PREFIX;
        analytics.sendEvent(client, analyticsPrefix + ".started");

        endpoint = "payment_methods/paypal_accounts";
        data = {
          paypalAccount: {
            billingAgreementToken: options.billingToken,
            merchantAccountId: self._merchantAccountId,
          },
        };

        if (!shouldVault) {
          data.paypalAccount.vault = false;
        }
      } else {
        analyticsPrefix = constants.ANALYTICS_EVENTS.TOKENIZE_PAYMENT_PREFIX;
        analytics.sendEventPlus(
          self._clientPromise,
          analyticsPrefix + ".started",
          {
            flow: self._flow,
            context_id: self._contextId, // eslint-disable-line camelcase
          }
        );

        data = self._formatTokenizeData({
          payerId: options.payerID,
          orderId: options.orderID,
        });
        endpoint = "payment_methods/paypal_accounts";
      }

      return client
        .request({
          endpoint: endpoint,
          method: "post",
          data: data,
        })
        .then(function (response) {
          var payload;

          payload = self._formatTokenizePayload(response);

          if (isBillingAgreement) {
            analytics.sendEvent(client, analyticsPrefix + ".succeeded");
          } else {
            if (payload.creditFinancingOffered) {
              analytics.sendEventPlus(
                self._clientPromise,
                constants.ANALYTICS_EVENTS.CREDIT_ACCEPTED,
                {
                  flow: self._flow,
                  context_id: self._contextId, // eslint-disable-line camelcase
                }
              );
            }

            analytics.sendEventPlus(
              self._clientPromise,
              analyticsPrefix + ".success",
              {
                flow: self._flow,
                context_id: self._contextId, // eslint-disable-line camelcase
              }
            );
          }

          return payload;
        })
        .catch(function (err) {
          if (isBillingAgreement) {
            analytics.sendEvent(client, analyticsPrefix + ".failed");

            throw new BraintreeError({
              type: errors.PAYPAL_CHECKOUT_V6_TOKENIZATION_FAILED.type,
              code: errors.PAYPAL_CHECKOUT_V6_TOKENIZATION_FAILED.code,
              message: errors.PAYPAL_CHECKOUT_V6_TOKENIZATION_FAILED.message,
              details: { originalError: err },
            });
          } else {
            analytics.sendEventPlus(
              self._clientPromise,
              analyticsPrefix + ".failed",
              {
                flow: self._flow,
                context_id: self._contextId, // eslint-disable-line camelcase
              }
            );

            if (self._setupError) {
              throw self._setupError;
            }

            throw convertToBraintreeError(err, {
              type: errors.PAYPAL_CHECKOUT_V6_TOKENIZATION_FAILED.type,
              code: errors.PAYPAL_CHECKOUT_V6_TOKENIZATION_FAILED.code,
              message: errors.PAYPAL_CHECKOUT_V6_TOKENIZATION_FAILED.message,
            });
          }
        })
        .then(resolve)
        .catch(reject);
    });
  });
};

/**
 * @private
 * @param {object} params Tokenization parameters.
 * @returns {object} Formatted data for tokenization request.
 */
PayPalCheckoutV6.prototype._formatTokenizeData = function (params) {
  var data = {
    paypalAccount: {
      paymentToken: params.paymentId || params.orderId,
      payerId: params.payerId,
      unilateral:
        this._configuration.gatewayConfiguration.paypal.unvettedMerchant,
    },
  };

  if (this._merchantAccountId) {
    data.merchantAccountId = this._merchantAccountId;
  }

  return data;
};

/**
 * @private
 * @param {object} response Response from tokenization request.
 * @returns {object} Formatted tokenization payload.
 */
PayPalCheckoutV6.prototype._formatTokenizePayload = function (response) {
  var payload;
  var account = {};

  if (response.paypalAccounts) {
    account = response.paypalAccounts[0];
  }

  payload = {
    nonce: account.nonce,
    details: {},
    type: account.type,
  };

  if (account.details && account.details.payerInfo) {
    payload.details = account.details.payerInfo;
  }

  if (account.details && account.details.creditFinancingOffered) {
    payload.creditFinancingOffered = account.details.creditFinancingOffered;
  }

  if (account.details && account.details.shippingOptionId) {
    payload.shippingOptionId = account.details.shippingOptionId;
  }

  if (account.details && account.details.cobrandedCardLabel) {
    payload.cobrandedCardLabel = account.details.cobrandedCardLabel;
  }

  return payload;
};

PayPalCheckoutV6.prototype._verifyConsistentCurrency = function (options) {
  var currencyMatches = true;
  var breakdown;
  var currencyFields = [
    "shippingCurrency",
    "handlingCurrency",
    "taxTotalCurrency",
    "insuranceCurrency",
    "shippingDiscountCurrency",
    "discountCurrency",
  ];

  if (!options.currency) {
    return true;
  }

  if (
    options.hasOwnProperty("shippingOptions") &&
    Array.isArray(options.shippingOptions)
  ) {
    currencyMatches = options.shippingOptions.every(function (item) {
      if (!item.amount || !item.amount.currency) {
        return false;
      }
      return (
        options.currency.toLowerCase() === item.amount.currency.toLowerCase()
      );
    });

    if (!currencyMatches) {
      return false;
    }
  }

  if (
    options.hasOwnProperty("lineItems") &&
    Array.isArray(options.lineItems) &&
    options.lineItems.length > 0
  ) {
    currencyMatches = options.lineItems.every(function (item) {
      if (item.unitTaxAmountCurrency) {
        return (
          options.currency.toLowerCase() ===
          item.unitTaxAmountCurrency.toLowerCase()
        );
      }
      return true;
    });

    if (!currencyMatches) {
      return false;
    }
  }

  if (options.hasOwnProperty("amountBreakdown") && options.amountBreakdown) {
    breakdown = options.amountBreakdown;

    currencyMatches = !currencyFields.some(function (field) {
      return (
        breakdown.hasOwnProperty(field) &&
        breakdown[field] &&
        options.currency.toLowerCase() !== breakdown[field].toLowerCase()
      );
    });
  }

  return currencyMatches;
};

/**
 * Updates a PayPal payment with new amounts or options.
 * @public
 * @param {object} options Update options
 * @param {string} options.paymentId The ID of the payment to update (orderId from session creation)
 * @param {string} options.amount The new payment amount
 * @param {string} options.currency The currency code (must match original currency)
 * @param {lineItem[]} [options.lineItems] Updated line items for the payment
 * @param {shippingOption[]} [options.shippingOptions] Updated shipping options
 * @param {object} [options.amountBreakdown] Breakdown of the amount
 * @param {callback} [callback] Optional callback. The callback is called with an error object as the first argument and the server response data as the second argument.
 * @example
 * // Promise-based approach - often used in onShippingAddressChange
 * paypal.Buttons({
 *   // ... other configuration ...
 *   onShippingAddressChange: function (data) {
 *     return paypalCheckoutV6Instance.updatePayment({
 *       paymentId: data.orderId,
 *       amount: '15.00',  // Updated total amount
 *       currency: 'USD',
 *       lineItems: [
 *         {
 *           quantity: '1',
 *           unitAmount: '10.00',
 *           name: 'Product Name',
 *           kind: 'debit'
 *         }
 *       ],
 *       shippingOptions: [
 *         {
 *           id: 'shipping-speed-fast',
 *           type: 'SHIPPING',
 *           label: 'Fast Shipping',
 *           selected: true,
 *           amount: {
 *             value: '5.00',
 *             currency: 'USD'
 *           }
 *         }
 *       ],
 *       amountBreakdown: {
 *         itemTotal: '10.00',
 *         shipping: '5.00',
 *         handling: '0.00',
 *         taxTotal: '0.00',
 *         insurance: '0.00',
 *         shippingDiscount: '0.00',
 *         discount: '0.00'
 *       }
 *     }).then(function(response) {
 *       console.log('Server response:', response);
 *     }).catch(function(err) {
 *       console.error('Update failed:', err);
 *     });
 *   }
 * });
 *
 * @example
 * // Callback-based approach
 * paypalCheckoutV6Instance.updatePayment({
 *   paymentId: orderId,
 *   amount: '15.00',
 *   currency: 'USD',
 *   lineItems: [{ ... }],
 *   shippingOptions: [{ ... }],
 *   amountBreakdown: { ... }
 * }, function (err, response) {
 *   if (err) {
 *     console.error('Update failed:', err);
 *     return;
 *   }
 *   console.log('Payment updated successfully:', response);
 * });
 *
 * @returns {Promise|undefined} Returns a promise if no callback is provided.
 */
PayPalCheckoutV6.prototype.updatePayment = function (options) {
  var self = this;
  var updatePayload;
  var isValidLineItems = true;

  if (!options || !options.paymentId || !options.amount || !options.currency) {
    return Promise.reject(
      new BraintreeError(errors.PAYPAL_CHECKOUT_V6_INVALID_UPDATE_OPTIONS)
    );
  }
  if (self._contextId !== options.paymentId) {
    return Promise.reject(
      new BraintreeError(errors.PAYPAL_CHECKOUT_V6_PAYMENT_NOT_FOUND)
    );
  }
  if (!self._verifyConsistentCurrency(options)) {
    return Promise.reject(
      new BraintreeError(errors.PAYPAL_CHECKOUT_V6_CURRENCY_MISMATCH)
    );
  }

  if (options.lineItems && Array.isArray(options.lineItems)) {
    isValidLineItems = options.lineItems.every(function (item) {
      return (
        item &&
        typeof item === "object" &&
        item.hasOwnProperty("quantity") &&
        item.hasOwnProperty("unitAmount") &&
        item.hasOwnProperty("name") &&
        item.hasOwnProperty("kind")
      );
    });

    if (!isValidLineItems) {
      return Promise.reject(
        new BraintreeError(errors.PAYPAL_CHECKOUT_V6_INVALID_LINE_ITEMS)
      );
    }
  }

  updatePayload = {
    amount: options.amount,
    currencyIsoCode: options.currency,
    paymentId: options.paymentId,
  };

  if (self._merchantAccountId) {
    updatePayload.merchantAccountId = self._merchantAccountId;
  }

  if (options.lineItems) {
    updatePayload.lineItems = options.lineItems;
  }

  if (options.shippingOptions) {
    updatePayload.shippingOptions = options.shippingOptions;
  }

  if (options.amountBreakdown) {
    updatePayload.amountBreakdown = options.amountBreakdown;
  }

  return self._clientPromise.then(function (client) {
    analytics.sendEvent(client, "paypal-checkout-v6.update-payment.started");

    return client
      .request({
        endpoint: "paypal_hermes/patch_payment_resource",
        method: "post",
        data: updatePayload,
      })
      .then(function (response) {
        analytics.sendEvent(
          client,
          "paypal-checkout-v6.update-payment.succeeded"
        );

        return {
          success: true,
          orderId: options.paymentId,
          updates: response.updates || {},
        };
      })
      .catch(function (err) {
        analytics.sendEvent(client, "paypal-checkout-v6.update-payment.failed");

        return Promise.reject(
          convertToBraintreeError(err, {
            type: errors.PAYPAL_CHECKOUT_V6_UPDATE_FAILED.type,
            code: errors.PAYPAL_CHECKOUT_V6_UPDATE_FAILED.code,
            message: errors.PAYPAL_CHECKOUT_V6_UPDATE_FAILED.message,
          })
        );
      });
  });
};

/**
 * @typedef {object} PayPalCheckoutV6~eligibilityResult
 * @property {boolean} paypal Whether standard PayPal payments are eligible.
 * @property {boolean} paylater Whether Pay Later (BNPL) is eligible.
 * @property {boolean} credit Whether PayPal Credit is eligible.
 */

/**
 * Finds eligible payment methods for the given amount and currency.
 * This allows merchants to check which payment methods (PayPal, Pay Later, PayPal Credit)
 * are eligible before rendering buttons, enabling dynamic UI that shows only available options.
 *
 * Eligibility depends on: currency, amount, merchant configuration, buyer location, and PayPal account features.
 *
 * @public
 * @param {object} options Eligibility check options.
 * @param {string} [options.amount] Optional payment amount (e.g., '10.00').
 * @param {string} options.currency The currency code (e.g., 'USD').
 * @example
 * // Check eligibility before rendering buttons
 * paypalCheckoutV6Instance.findEligibleMethods({
 *   amount: '10.00',
 *   currency: 'USD'
 * }).then(function (eligibility) {
 *   if (eligibility.paylater) {
 *     // Show Pay Later button
 *     document.getElementById('paylater-button').style.display = 'block';
 *   }
 *   if (eligibility.credit) {
 *     // Show PayPal Credit button
 *     document.getElementById('credit-button').style.display = 'block';
 *   }
 *   if (eligibility.paypal) {
 *     // Show standard PayPal button
 *     document.getElementById('paypal-button').style.display = 'block';
 *   }
 * }).catch(function (err) {
 *   console.error('Eligibility check failed:', err);
 * });
 *
 * @example
 * // Conditionally offer Pay Later messaging
 * paypalCheckoutV6Instance.findEligibleMethods({
 *   amount: '150.00',
 *   currency: 'USD'
 * }).then(function (eligibility) {
 *   if (eligibility.paylater) {
 *     // Show "Pay in 4" promotional messaging
 *     showPayLaterPromo();
 *   }
 * });
 *
 * @returns {Promise<PayPalCheckoutV6~eligibilityResult>} A promise that resolves with eligibility flags for each payment method.
 */
PayPalCheckoutV6.prototype.findEligibleMethods = function (options) {
  var self = this;

  return this._clientPromise.then(function (client) {
    if (!options || !options.currency) {
      return Promise.reject(
        new BraintreeError(
          errors.PAYPAL_CHECKOUT_V6_INVALID_ELIGIBILITY_OPTIONS
        )
      );
    }

    // Get or create the PayPal instance
    var instancePromise = self._paypalInstance
      ? Promise.resolve(self._paypalInstance)
      : self._createPayPalInstance();

    return instancePromise.then(function (paypalInstance) {
      if (
        !paypalInstance ||
        typeof paypalInstance.findEligibleMethods !== "function"
      ) {
        return Promise.reject(
          new BraintreeError(errors.PAYPAL_CHECKOUT_V6_SDK_NOT_INITIALIZED)
        );
      }

      analytics.sendEvent(
        client,
        constants.ANALYTICS_EVENTS.FIND_ELIGIBLE_METHODS_STARTED
      );

      return paypalInstance
        .findEligibleMethods({
          currencyCode: options.currency,
          amount: options.amount,
        })
        .then(function (paymentMethods) {
          analytics.sendEvent(
            client,
            constants.ANALYTICS_EVENTS.FIND_ELIGIBLE_METHODS_SUCCEEDED
          );

          // Check if SDK returns isEligible method or direct boolean properties
          var hasIsEligible =
            paymentMethods && typeof paymentMethods.isEligible === "function";

          return {
            paypal: hasIsEligible
              ? paymentMethods.isEligible("paypal")
              : Boolean(paymentMethods && paymentMethods.paypal),
            paylater: hasIsEligible
              ? paymentMethods.isEligible("paylater")
              : Boolean(paymentMethods && paymentMethods.paylater),
            credit: hasIsEligible
              ? paymentMethods.isEligible("credit")
              : Boolean(paymentMethods && paymentMethods.credit),
          };
        })
        .catch(function (err) {
          analytics.sendEvent(
            client,
            constants.ANALYTICS_EVENTS.FIND_ELIGIBLE_METHODS_FAILED
          );

          throw convertToBraintreeError(err, {
            type: errors.PAYPAL_CHECKOUT_V6_ELIGIBILITY_CHECK_FAILED.type,
            code: errors.PAYPAL_CHECKOUT_V6_ELIGIBILITY_CHECK_FAILED.code,
            message: errors.PAYPAL_CHECKOUT_V6_ELIGIBILITY_CHECK_FAILED.message,
          });
        });
    });
  });
};

/**
 * Cleanly tears down the PayPal Checkout V6 component.
 * @public
 * @example
 * paypalCheckoutV6Instance.teardown();
 * @returns {Promise} Returns a promise that resolves when teardown is complete.
 */
PayPalCheckoutV6.prototype.teardown = function () {
  convertMethodsToError(this, methods(PayPalCheckoutV6.prototype));

  if (this._paypalScript && this._paypalScript.parentNode) {
    this._paypalScript.parentNode.removeChild(this._paypalScript);
  }

  return this._clientPromise.then(function (client) {
    analytics.sendEvent(client, constants.ANALYTICS_EVENTS.TEARDOWN);
  });
};

module.exports = wrapPromise.wrapPrototype(PayPalCheckoutV6);