'use strict';
var BraintreeError = require('../../lib/braintree-error');
var analytics = require('../../lib/analytics');
var assign = require('../../lib/assign').assign;
var methods = require('../../lib/methods');
var convertMethodsToError = require('../../lib/convert-methods-to-error');
var constants = require('../shared/constants');
var useMin = require('../../lib/use-min');
var Bus = require('../../lib/bus');
var uuid = require('../../lib/vendor/uuid');
var deferred = require('../../lib/deferred');
var errors = require('../shared/errors');
var events = require('../shared/events');
var VERSION = process.env.npm_package_version;
var iFramer = require('@braintree/iframer');
var Promise = require('../../lib/promise');
var wrapPromise = require('@braintree/wrap-promise');
var IFRAME_HEIGHT = 400;
var IFRAME_WIDTH = 400;
/**
* @typedef {object} ThreeDSecure~verifyPayload
* @property {string} nonce The new payment method nonce produced by the 3D Secure lookup. The original nonce passed into {@link ThreeDSecure#verifyCard|verifyCard} was consumed. This new nonce should be used to transact on your server.
* @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 {string} description A human-readable description.
* @property {boolean} liabilityShiftPossible Indicates whether the card was eligible for 3D Secure.
* @property {boolean} liabilityShifted Indicates whether the liability for fraud has been shifted away from the merchant.
*/
/**
* @class
* @param {object} options 3D Secure {@link module:braintree-web/three-d-secure.create create} options
* @description <strong>Do not use this constructor directly. Use {@link module:braintree-web/three-d-secure.create|braintree.threeDSecure.create} instead.</strong>
* @classdesc This class represents a ThreeDSecure component produced by {@link module:braintree-web/three-d-secure.create|braintree.threeDSecure.create}. Instances of this class have a method for launching a 3D Secure authentication flow.
*/
function ThreeDSecure(options) {
this._options = options;
this._assetsUrl = options.client.getConfiguration().gatewayConfiguration.assetsUrl;
this._isDebug = options.client.getConfiguration().isDebug;
this._client = options.client;
}
/**
* @callback ThreeDSecure~addFrameCallback
* @param {?BraintreeError} [err] `null` or `undefined` if there was no error.
* @param {HTMLIFrameElement} iframe An iframe element containing the bank's authentication page that you must put on your page.
* @description The callback used for options.addFrame in {@link ThreeDSecure#verifyCard|verifyCard}.
* @returns {void}
*/
/**
* @callback ThreeDSecure~removeFrameCallback
* @description The callback used for options.removeFrame in {@link ThreeDSecure#verifyCard|verifyCard}.
* @returns {void}
*/
/**
* Launch the 3D Secure login flow, returning a nonce payload.
* @public
* @param {object} options Options for card verification.
* @param {string} options.nonce A nonce referencing the card to be verified. For example, this can be a nonce that was returned by Hosted Fields.
* @param {number} options.amount The amount of the transaction in the current merchant account's currency. For example, if you are running a transaction of $123.45 US dollars, `amount` would be 123.45.
* @param {callback} options.addFrame This {@link ThreeDSecure~addFrameCallback|addFrameCallback} will be called when the bank frame needs to be added to your page.
* @param {callback} options.removeFrame This {@link ThreeDSecure~removeFrameCallback|removeFrameCallback} will be called when the bank frame needs to be removed from your page.
* @param {string} [options.customer.mobilePhoneNumber] The mobile phone number used for verification. Only numbers; remove dashes, paranthesis and other characters.
* @param {string} [options.customer.email] The email used for verification.
* @param {string} [options.customer.shippingMethod] The 2-digit string indicating the shipping method chosen for the transaction.
* @param {string} [options.customer.billingAddress.firstName] The first name associated with the address.
* @param {string} [options.customer.billingAddress.lastName] The last name associated with the address.
* @param {string} [options.customer.billingAddress.streetAddress] Line 1 of the Address (eg. number, street, etc).
* @param {string} [options.customer.billingAddress.extendedAddress] Line 2 of the Address (eg. suite, apt #, etc.).
* @param {string} [options.customer.billingAddress.locality] The locality (city) name associated with the address.
* @param {string} [options.customer.billingAddress.region] The 2 letter code for US states, and the equivalent for other countries.
* @param {string} [options.customer.billingAddress.postalCode] The zip code or equivalent for countries that have them.
* @param {string} [options.customer.billingAddress.countryCodeAlpha2] The 2 character country code.
* @param {string} [options.customer.billingAddress.phoneNumber] The phone number associated with the address. Only numbers; remove dashes, paranthesis and other characters.
* @param {boolean} [options.showLoader=true] Whether to show the loader icon while the bank frame is loading.
* @param {callback} [callback] The second argument, <code>data</code>, is a {@link ThreeDSecure~verifyPayload|verifyPayload}. If no callback is provided, it will return a promise that resolves {@link ThreeDSecure~verifyPayload|verifyPayload}.
* @returns {Promise|void} Returns a promise if no callback is provided.
* @example
* <caption>Verifying an existing nonce with 3DS</caption>
* var my3DSContainer;
*
* threeDSecure.verifyCard({
* nonce: existingNonce,
* amount: 123.45,
* addFrame: function (err, iframe) {
* // Set up your UI and add the iframe.
* my3DSContainer = document.createElement('div');
* my3DSContainer.appendChild(iframe);
* document.body.appendChild(my3DSContainer);
* },
* removeFrame: function () {
* // Remove UI that you added in addFrame.
* document.body.removeChild(my3DSContainer);
* }
* }, function (err, payload) {
* if (err) {
* console.error(err);
* return;
* }
*
* if (payload.liabilityShifted) {
* // Liablity has shifted
* submitNonceToServer(payload.nonce);
* } else if (payload.liabilityShiftPossible) {
* // Liablity may still be shifted
* // Decide if you want to submit the nonce
* } else {
* // Liablity has not shifted and will not shift
* // Decide if you want to submit the nonce
* }
* });
*/
ThreeDSecure.prototype.verifyCard = function (options) {
var url, showLoader, addFrame, removeFrame, error, errorOption;
var self = this;
options = assign({}, options);
if (options.customer && options.customer.billingAddress) {
// map from public API to the API that the Gateway expects
options.customer.billingAddress.line1 = options.customer.billingAddress.streetAddress;
options.customer.billingAddress.line2 = options.customer.billingAddress.extendedAddress;
options.customer.billingAddress.city = options.customer.billingAddress.locality;
options.customer.billingAddress.state = options.customer.billingAddress.region;
options.customer.billingAddress.countryCode = options.customer.billingAddress.countryCodeAlpha2;
delete options.customer.billingAddress.streetAddress;
delete options.customer.billingAddress.extendedAddress;
delete options.customer.billingAddress.locality;
delete options.customer.billingAddress.region;
delete options.customer.billingAddress.countryCodeAlpha2;
}
if (this._verifyCardInProgress === true) {
error = errors.THREEDS_AUTHENTICATION_IN_PROGRESS;
} else if (!options.nonce) {
errorOption = 'a nonce';
} else if (!options.amount) {
errorOption = 'an amount';
} else if (typeof options.addFrame !== 'function') {
errorOption = 'an addFrame function';
} else if (typeof options.removeFrame !== 'function') {
errorOption = 'a removeFrame function';
}
if (errorOption) {
error = {
type: errors.THREEDS_MISSING_VERIFY_CARD_OPTION.type,
code: errors.THREEDS_MISSING_VERIFY_CARD_OPTION.code,
message: 'verifyCard options must include ' + errorOption + '.'
};
}
if (error) {
return Promise.reject(new BraintreeError(error));
}
showLoader = options.showLoader !== false;
this._verifyCardInProgress = true;
addFrame = deferred(options.addFrame);
removeFrame = deferred(options.removeFrame);
url = 'payment_methods/' + options.nonce + '/three_d_secure/lookup';
return this._client.request({
endpoint: url,
method: 'post',
data: {amount: options.amount, customer: options.customer}
}).then(function (response) {
self._lookupPaymentMethod = response.paymentMethod;
return new Promise(function (resolve, reject) {
self._verifyCardCallback = function (verifyErr, payload) {
self._verifyCardInProgress = false;
if (verifyErr) {
reject(verifyErr);
} else {
resolve(payload);
}
};
self._handleLookupResponse({
showLoader: showLoader,
lookupResponse: response,
addFrame: addFrame,
removeFrame: removeFrame
});
});
}).catch(function (err) {
self._verifyCardInProgress = false;
return Promise.reject(err);
});
};
/**
* Cancel the 3DS flow and return the verification payload if available.
* @public
* @param {callback} [callback] The second argument is a {@link ThreeDSecure~verifyPayload|verifyPayload}. If there is no verifyPayload (the initial lookup did not complete), an error will be returned. If no callback is passed, `cancelVerifyCard` will return a promise.
* @returns {Promise|void} Returns a promise if no callback is provided.
* @example
* threeDSecure.cancelVerifyCard(function (err, verifyPayload) {
* if (err) {
* // Handle error
* console.log(err.message); // No verification payload available
* return;
* }
*
* verifyPayload.nonce; // The nonce returned from the 3ds lookup call
* verifyPayload.liabilityShifted; // boolean
* verifyPayload.liabilityShiftPossible; // boolean
* });
*/
ThreeDSecure.prototype.cancelVerifyCard = function () {
var response;
this._verifyCardInProgress = false;
if (!this._lookupPaymentMethod) {
return Promise.reject(new BraintreeError(errors.THREEDS_NO_VERIFICATION_PAYLOAD));
}
response = assign({}, this._lookupPaymentMethod, {
liabilityShiftPossible: this._lookupPaymentMethod.threeDSecureInfo.liabilityShiftPossible,
liabilityShifted: this._lookupPaymentMethod.threeDSecureInfo.liabilityShifted,
verificationDetails: this._lookupPaymentMethod.threeDSecureInfo.verificationDetails
});
return Promise.resolve(response);
};
ThreeDSecure.prototype._handleLookupResponse = function (options) {
var lookupResponse = options.lookupResponse;
if (lookupResponse.lookup && lookupResponse.lookup.acsUrl && lookupResponse.lookup.acsUrl.length > 0) {
options.addFrame(null, this._createIframe({
showLoader: options.showLoader,
response: lookupResponse.lookup,
removeFrame: options.removeFrame
}));
} else {
this._verifyCardCallback(null, {
nonce: lookupResponse.paymentMethod.nonce,
liabilityShiftPossible: lookupResponse.threeDSecureInfo.liabilityShiftPossible,
liabilityShifted: lookupResponse.threeDSecureInfo.liabilityShifted,
verificationDetails: lookupResponse.threeDSecureInfo
});
}
};
ThreeDSecure.prototype._createIframe = function (options) {
var url, authenticationCompleteBaseUrl;
var parentURL = window.location.href;
var response = options.response;
this._bus = new Bus({
channel: uuid(),
merchantUrl: location.href
});
authenticationCompleteBaseUrl = this._assetsUrl + '/web/' + VERSION + '/html/three-d-secure-authentication-complete-frame.html?channel=' + encodeURIComponent(this._bus.channel) + '&';
if (parentURL.indexOf('#') > -1) {
parentURL = parentURL.split('#')[0];
}
this._bus.on(Bus.events.CONFIGURATION_REQUEST, function (reply) {
reply({
acsUrl: response.acsUrl,
pareq: response.pareq,
termUrl: response.termUrl + '&three_d_secure_version=' + VERSION + '&authentication_complete_base_url=' + encodeURIComponent(authenticationCompleteBaseUrl),
md: response.md,
parentUrl: parentURL
});
});
this._bus.on(events.AUTHENTICATION_COMPLETE, function (data) {
this._handleAuthResponse(data, options);
}.bind(this));
url = this._assetsUrl + '/web/' + VERSION + '/html/three-d-secure-bank-frame' + useMin(this._isDebug) + '.html?showLoader=' + options.showLoader;
this._bankIframe = iFramer({
src: url,
height: IFRAME_HEIGHT,
width: IFRAME_WIDTH,
name: constants.LANDING_FRAME_NAME + '_' + this._bus.channel
});
return this._bankIframe;
};
ThreeDSecure.prototype._handleAuthResponse = function (data, options) {
var authResponse = JSON.parse(data.auth_response);
this._bus.teardown();
options.removeFrame();
// This also has to be in a setTimeout so it executes after the `removeFrame`.
deferred(function () {
if (authResponse.success) {
this._verifyCardCallback(null, this._formatAuthResponse(authResponse.paymentMethod, authResponse.threeDSecureInfo));
} else if (authResponse.threeDSecureInfo && authResponse.threeDSecureInfo.liabilityShiftPossible) {
this._verifyCardCallback(null, this._formatAuthResponse(this._lookupPaymentMethod, authResponse.threeDSecureInfo));
} else {
this._verifyCardCallback(new BraintreeError({
type: BraintreeError.types.UNKNOWN,
code: 'UNKNOWN_AUTH_RESPONSE',
message: authResponse.error.message
}));
}
}.bind(this))();
};
ThreeDSecure.prototype._formatAuthResponse = function (paymentMethod, threeDSecureInfo) {
return {
nonce: paymentMethod.nonce,
details: paymentMethod.details,
description: paymentMethod.description,
liabilityShifted: threeDSecureInfo.liabilityShifted,
liabilityShiftPossible: threeDSecureInfo.liabilityShiftPossible
};
};
/**
* Cleanly remove anything set up by {@link module:braintree-web/three-d-secure.create|create}.
* @public
* @param {callback} [callback] Called on completion. If no callback is passed, `teardown` will return a promise.
* @example
* threeDSecure.teardown();
* @example <caption>With callback</caption>
* threeDSecure.teardown(function () {
* // teardown is complete
* });
* @returns {Promise|void} Returns a promise if no callback is provided.
*/
ThreeDSecure.prototype.teardown = function () {
var iframeParent;
convertMethodsToError(this, methods(ThreeDSecure.prototype));
analytics.sendEvent(this._options.client, 'threedsecure.teardown-completed');
if (this._bus) {
this._bus.teardown();
}
if (this._bankIframe) {
iframeParent = this._bankIframe.parentNode;
if (iframeParent) {
iframeParent.removeChild(this._bankIframe);
}
}
return Promise.resolve();
};
module.exports = wrapPromise.wrapPrototype(ThreeDSecure);