'use strict';
var Promise = require('../../lib/promise');
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);