"use strict";
var frameService = require("../../lib/frame-service/external");
var BraintreeError = require("../../lib/braintree-error");
var errors = require("../shared/errors");
var VERSION = process.env.npm_package_version;
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);