'use strict';
var BraintreeError = require('../../lib/error');
var analytics = require('../../lib/analytics');
var methods = require('../../lib/methods');
var convertMethodsToError = require('../../lib/convert-methods-to-error');
var constants = require('../shared/constants');
var Bus = require('../../lib/bus');
var uuid = require('../../lib/uuid');
var deferred = require('../../lib/deferred');
var errors = require('../shared/errors');
var throwIfNoCallback = require('../../lib/throw-if-no-callback');
var events = require('../shared/events');
var version = require('package.version');
var iFramer = require('iframer');
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.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._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 {errback} 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 {errback} callback The second argument, <code>data</code>, is a {@link ThreeDSecure~verifyPayload|verifyPayload}
* @returns {void}
* @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, callback) {
var url, addFrame, removeFrame, error, errorOption;
throwIfNoCallback(callback, 'verifyCard');
options = options || {};
callback = deferred(callback);
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) {
callback(new BraintreeError(error));
return;
}
this._verifyCardInProgress = true;
addFrame = deferred(options.addFrame);
removeFrame = deferred(options.removeFrame);
url = 'payment_methods/' + options.nonce + '/three_d_secure/lookup';
this._client.request({
endpoint: url,
method: 'post',
data: {amount: options.amount}
}, function (err, response) {
if (err) {
this._verifyCardInProgress = false;
callback(err);
return;
}
this._lookupPaymentMethod = response.paymentMethod;
this._verifyCardCallback = function () {
this._verifyCardInProgress = false;
callback.apply(null, arguments);
}.bind(this);
this._handleLookupResponse({
lookupResponse: response,
addFrame: addFrame,
removeFrame: removeFrame
});
}.bind(this));
};
/**
* Cancel the 3DS flow and return the verification payload if available.
* @public
* @param {errback} 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.
* @returns {void}
* @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 (callback) {
var error;
this._verifyCardInProgress = false;
if (typeof callback === 'function') {
if (!this._lookupPaymentMethod) {
error = new BraintreeError(errors.THREEDS_NO_VERIFICATION_PAYLOAD);
}
callback(error, this._lookupPaymentMethod);
}
};
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({
response: lookupResponse.lookup,
removeFrame: options.removeFrame
}));
} else {
this._verifyCardCallback(null, {
nonce: lookupResponse.paymentMethod.nonce,
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@DOT_MIN.html';
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 tear down anything set up by {@link module:braintree-web/three-d-secure.create|create}
* @public
* @param {errback} [callback] Called once teardown is complete. No data is returned if teardown completes successfully.
* @returns {void}
*/
ThreeDSecure.prototype.teardown = function (callback) {
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);
}
}
if (typeof callback === 'function') {
callback = deferred(callback);
callback();
}
};
module.exports = ThreeDSecure;