UNPKG

braintree-web

Version:

A suite of tools for integrating Braintree in the browser

474 lines (416 loc) 16.4 kB
"use strict"; var frameService = require("../../lib/frame-service/external"); var BraintreeError = require("../../lib/braintree-error"); var errors = require("../shared/errors"); var VERSION = "3.117.1"; var methods = require("../../lib/methods"); var wrapPromise = require("@braintree/wrap-promise"); var analytics = require("../../lib/analytics"); var convertMethodsToError = require("../../lib/convert-methods-to-error"); var convertToBraintreeError = require("../../lib/convert-to-braintree-error"); var constants = require("../shared/constants"); var INTEGRATION_TIMEOUT_MS = require("../../lib/constants").INTEGRATION_TIMEOUT_MS; /** * Masterpass Address object. * @typedef {object} Masterpass~Address * @property {string} countryCodeAlpha2 The customer's country code. * @property {string} extendedAddress The customer's extended address. * @property {string} locality The customer's locality. * @property {string} postalCode The customer's postal code. * @property {string} region The customer's region. * @property {string} streetAddress The customer's street address. */ /** * @typedef {object} Masterpass~tokenizePayload * @property {string} nonce The payment method nonce. * @property {string} description The human readable description. * @property {string} type The payment method type, always `MasterpassCard`. * @property {object} details Additional account details. * @property {string} details.cardType Type of card, ex: Visa, MasterCard. * @property {string} details.lastFour Last four digits of card number. * @property {string} details.lastTwo Last two digits of card number. * @property {object} contact The customer's contact information. * @property {string} contact.firstName The customer's first name. * @property {string} contact.lastName The customer's last name. * @property {string} contact.phoneNumber The customer's phone number. * @property {string} contact.emailAddress The customer's email address. * @property {Masterpass~Address} billingAddress The customer's billing address. * @property {Masterpass~Address} shippingAddress The customer's shipping address. * @property {object} binData Information about the card based on the bin. * @property {string} binData.commercial Possible values: 'Yes', 'No', 'Unknown'. * @property {string} binData.countryOfIssuance The country of issuance. * @property {string} binData.debit Possible values: 'Yes', 'No', 'Unknown'. * @property {string} binData.durbinRegulated Possible values: 'Yes', 'No', 'Unknown'. * @property {string} binData.healthcare Possible values: 'Yes', 'No', 'Unknown'. * @property {string} binData.issuingBank The issuing bank. * @property {string} binData.payroll Possible values: 'Yes', 'No', 'Unknown'. * @property {string} binData.prepaid Possible values: 'Yes', 'No', 'Unknown'. * @property {string} binData.productId The product id. */ /** * @class * @param {object} options see {@link module:braintree-web/masterpass.create|masterpass.create} * @description <strong>You cannot use this constructor directly. Use {@link module:braintree-web/masterpass.create|braintree.masterpass.create} instead.</strong> * @classdesc This class represents an Masterpass component. Instances of this class have methods for launching a new window to process a transaction with Masterpass. */ function Masterpass(options) { var configuration = options.client.getConfiguration(); this._client = options.client; this._assetsUrl = configuration.gatewayConfiguration.assetsUrl + "/web/" + VERSION; this._isDebug = configuration.isDebug; this._authInProgress = false; if ( window.popupBridge && typeof window.popupBridge.getReturnUrlPrefix === "function" ) { this._callbackUrl = window.popupBridge.getReturnUrlPrefix() + "return"; } else { this._callbackUrl = this._assetsUrl + "/html/redirect-frame" + (this._isDebug ? "" : ".min") + ".html"; } } Masterpass.prototype._initialize = function () { var self = this; return new Promise(function (resolve) { var failureTimeout = setTimeout(function () { analytics.sendEvent(self._client, "masterpass.load.timed-out"); }, INTEGRATION_TIMEOUT_MS); frameService.create( { name: constants.LANDING_FRAME_NAME, height: constants.POPUP_HEIGHT, width: constants.POPUP_WIDTH, dispatchFrameUrl: self._assetsUrl + "/html/dispatch-frame" + (self._isDebug ? "" : ".min") + ".html", openFrameUrl: self._assetsUrl + "/html/masterpass-landing-frame" + (self._isDebug ? "" : ".min") + ".html", }, function (service) { self._frameService = service; clearTimeout(failureTimeout); analytics.sendEvent(self._client, "masterpass.load.succeeded"); resolve(self); } ); }); }; /** * Launches the Masterpass flow and returns a nonce payload. Only one Masterpass flow should be active at a time. One way to achieve this is to disable your Masterpass button while the flow is open. * * Braintree will apply these properties in `options.config`. Merchants should not override these values, except for advanced usage. * - `environment` * - `requestToken` * - `callbackUrl` * - `merchantCheckoutId` * - `allowedCardTypes` * - `version` * * @public * @param {object} options All options for initiating the Masterpass payment flow. * @param {string} options.currencyCode The currency code to process the payment. * @param {string} options.subtotal The amount to authorize for the transaction. * @param {object} [options.config] All configuration parameters accepted by Masterpass lightbox, except `function` data type. These options will override the values set by Braintree server. Please see {@link Masterpass Lightbox Parameters|https://developer.mastercard.com/page/masterpass-lightbox-parameters} for more information. * @param {object} [options.frameOptions] Used to configure the window that contains the Masterpass login. * @param {number} [options.frameOptions.width] Popup width to be used instead of default value (450px). * @param {number} [options.frameOptions.height] Popup height to be used instead of default value (660px). * @param {number} [options.frameOptions.top] The top position of the popup window to be used instead of default value, that is calculated based on provided height, and parent window size. * @param {number} [options.frameOptions.left] The left position to the popup window to be used instead of default value, that is calculated based on provided width, and parent window size. * @param {callback} [callback] The second argument, <code>data</code>, is a {@link Masterpass~tokenizePayload|tokenizePayload}. If no callback is provided, the method will return a Promise that resolves with a {@link Masterpass~tokenizePayload|tokenizePayload}. * @returns {(Promise|void)} Returns a promise if no callback is provided. * @example * button.addEventListener('click', function () { * // Disable the button so that we don't attempt to open multiple popups. * button.setAttribute('disabled', 'disabled'); * * // Because tokenize opens a new window, this must be called * // as a result of a user action, such as a button click. * masterpassInstance.tokenize({ * currencyCode: 'USD', * subtotal: '10.00' * }).then(function (payload) { * button.removeAttribute('disabled'); * // Submit payload.nonce to your server * }).catch(function (tokenizeError) { * button.removeAttribute('disabled'); * // Handle flow errors or premature flow closure * * switch (tokenizeErr.code) { * case 'MASTERPASS_POPUP_CLOSED': * console.error('Customer closed Masterpass popup.'); * break; * case 'MASTERPASS_ACCOUNT_TOKENIZATION_FAILED': * console.error('Masterpass tokenization failed. See details:', tokenizeErr.details); * break; * case 'MASTERPASS_FLOW_FAILED': * console.error('Unable to initialize Masterpass flow. Are your options correct?', tokenizeErr.details); * break; * default: * console.error('Error!', tokenizeErr); * } * }); * }); */ Masterpass.prototype.tokenize = function (options) { var self = this; if (!options || hasMissingOption(options)) { return Promise.reject( new BraintreeError(errors.MASTERPASS_TOKENIZE_MISSING_REQUIRED_OPTION) ); } if (self._authInProgress) { return Promise.reject( new BraintreeError(errors.MASTERPASS_TOKENIZATION_ALREADY_IN_PROGRESS) ); } return new Promise(function (resolve, reject) { self._navigateFrameToLoadingPage(options).catch(reject); // This MUST happen after _navigateFrameToLoadingPage for Metro browsers to work. self._frameService.open( options.frameOptions, self._createFrameOpenHandler(resolve, reject) ); }); }; Masterpass.prototype._navigateFrameToLoadingPage = function (options) { var self = this; this._authInProgress = true; return this._client .request({ method: "post", endpoint: "masterpass/request_token", data: { requestToken: { originUrl: window.location.protocol + "//" + window.location.hostname, subtotal: options.subtotal, currencyCode: options.currencyCode, callbackUrl: this._callbackUrl, }, }, }) .then(function (response) { var redirectUrl = self._assetsUrl + "/html/masterpass-loading-frame" + (self._isDebug ? "" : ".min") + ".html?"; var gatewayConfiguration = self._client.getConfiguration().gatewayConfiguration; var config = options.config || {}; var queryParams; queryParams = { environment: gatewayConfiguration.environment, requestToken: response.requestToken, callbackUrl: self._callbackUrl, merchantCheckoutId: gatewayConfiguration.masterpass.merchantCheckoutId, allowedCardTypes: gatewayConfiguration.masterpass.supportedNetworks, version: constants.MASTERPASS_VERSION, }; Object.keys(config).forEach(function (key) { if (typeof config[key] !== "function") { queryParams[key] = config[key]; } }); redirectUrl += Object.keys(queryParams) .map(function (key) { return key + "=" + queryParams[key]; }) .join("&"); self._frameService.redirect(redirectUrl); }) .catch(function (err) { var status = err.details && err.details.httpStatus; self._closeWindow(); if (status === 422) { return Promise.reject( convertToBraintreeError(err, errors.MASTERPASS_INVALID_PAYMENT_OPTION) ); } return Promise.reject( convertToBraintreeError(err, errors.MASTERPASS_FLOW_FAILED) ); }); }; Masterpass.prototype._createFrameOpenHandler = function (resolve, reject) { var self = this; if (window.popupBridge) { return function (popupBridgeErr, payload) { self._authInProgress = false; if (popupBridgeErr) { analytics.sendEvent( self._client, "masterpass.tokenization.closed-popupbridge.by-user" ); reject( convertToBraintreeError( popupBridgeErr, errors.MASTERPASS_POPUP_CLOSED ) ); return; } else if (!payload.queryItems) { analytics.sendEvent( self._client, "masterpass.tokenization.failed-popupbridge" ); reject(new BraintreeError(errors.MASTERPASS_FLOW_FAILED)); return; } self._tokenizeMasterpass(payload.queryItems).then(resolve).catch(reject); }; } return function (frameServiceErr, payload) { if (frameServiceErr) { self._authInProgress = false; if (frameServiceErr.code === "FRAME_SERVICE_FRAME_CLOSED") { analytics.sendEvent( self._client, "masterpass.tokenization.closed.by-user" ); reject(new BraintreeError(errors.MASTERPASS_POPUP_CLOSED)); return; } if ( frameServiceErr.code && frameServiceErr.code.indexOf("FRAME_SERVICE_FRAME_OPEN_FAILED") > -1 ) { analytics.sendEvent( self._client, "masterpass.tokenization.failed.to-open" ); reject( new BraintreeError({ code: errors.MASTERPASS_POPUP_OPEN_FAILED.code, type: errors.MASTERPASS_POPUP_OPEN_FAILED.type, message: errors.MASTERPASS_POPUP_OPEN_FAILED.message, details: { originalError: frameServiceErr, }, }) ); return; } analytics.sendEvent(self._client, "masterpass.tokenization.failed"); self._closeWindow(); reject( convertToBraintreeError(frameServiceErr, errors.MASTERPASS_FLOW_FAILED) ); return; } self._tokenizeMasterpass(payload).then(resolve).catch(reject); }; }; Masterpass.prototype._tokenizeMasterpass = function (payload) { var self = this; if (payload.mpstatus !== "success") { analytics.sendEvent(self._client, "masterpass.tokenization.closed.by-user"); self._closeWindow(); return Promise.reject(new BraintreeError(errors.MASTERPASS_POPUP_CLOSED)); } if (isMissingRequiredPayload(payload)) { analytics.sendEvent( self._client, "masterpass.tokenization.closed.missing-payload" ); self._closeWindow(); return Promise.reject( new BraintreeError(errors.MASTERPASS_POPUP_MISSING_REQUIRED_PARAMETERS) ); } return self._client .request({ endpoint: "payment_methods/masterpass_cards", method: "post", data: { masterpassCard: { checkoutResourceUrl: payload.checkout_resource_url, requestToken: payload.oauth_token, verifierToken: payload.oauth_verifier, }, }, }) .then(function (response) { self._closeWindow(); if (window.popupBridge) { analytics.sendEvent( self._client, "masterpass.tokenization.success-popupbridge" ); } else { analytics.sendEvent(self._client, "masterpass.tokenization.success"); } return response.masterpassCards[0]; }) .catch(function (tokenizeErr) { self._closeWindow(); if (window.popupBridge) { analytics.sendEvent( self._client, "masterpass.tokenization.failed-popupbridge" ); } else { analytics.sendEvent(self._client, "masterpass.tokenization.failed"); } return Promise.reject( convertToBraintreeError( tokenizeErr, errors.MASTERPASS_ACCOUNT_TOKENIZATION_FAILED ) ); }); }; function isMissingRequiredPayload(payload) { return [ payload.oauth_verifier, payload.oauth_token, payload.checkout_resource_url, ].some(function (element) { return element == null || element === "null"; }); } Masterpass.prototype._closeWindow = function () { this._authInProgress = false; this._frameService.close(); }; /** * Cleanly tear down anything set up by {@link module:braintree-web/masterpass.create|create}. * @public * @param {callback} [callback] Called on completion. If no callback is provided, `teardown` returns a promise. * @example * masterpassInstance.teardown(); * @example <caption>With callback</caption> * masterpassInstance.teardown(function () { * // teardown is complete * }); * @returns {(Promise|void)} Returns a promise if no callback is provided. */ Masterpass.prototype.teardown = function () { var self = this; return new Promise(function (resolve) { self._frameService.teardown(); convertMethodsToError(self, methods(Masterpass.prototype)); analytics.sendEvent(self._client, "masterpass.teardown-completed"); resolve(); }); }; function hasMissingOption(options) { var i, option; for (i = 0; i < constants.REQUIRED_OPTIONS_FOR_TOKENIZE.length; i++) { option = constants.REQUIRED_OPTIONS_FOR_TOKENIZE[i]; if (!options.hasOwnProperty(option)) { return true; } } return false; } module.exports = wrapPromise.wrapPrototype(Masterpass);