"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);