braintree-web
Version:
A suite of tools for integrating Braintree in the browser
493 lines (414 loc) • 13.8 kB
JavaScript
"use strict";
var assign = require("../../../lib/assign").assign;
var analytics = require("../../../lib/analytics");
var BraintreeError = require("../../../lib/braintree-error");
var isVerifiedDomain = require("../../../lib/is-verified-domain");
var ExtendedPromise = require("@braintree/extended-promise");
var EventEmitter = require("@braintree/event-emitter");
var errors = require("../../shared/errors");
var iFramer = require("@braintree/iframer");
var Bus = require("framebus");
var constants = require("../../shared/constants");
var uuid = require("@braintree/uuid");
var events = require("../../shared/events");
var useMin = require("../../../lib/use-min");
var BUS_CONFIGURATION_REQUEST_EVENT =
require("../../../lib/constants").BUS_CONFIGURATION_REQUEST_EVENT;
var VERSION = "3.117.1";
var IFRAME_HEIGHT = 400;
var IFRAME_WIDTH = 400;
ExtendedPromise.suppressUnhandledPromiseMessage = true;
function BaseFramework(options) {
EventEmitter.call(this);
this._client = options.client;
this._createPromise = options.createPromise;
this._createOptions = options;
if (this._client) {
this._isDebug = this._client.getConfiguration().isDebug;
this._assetsUrl =
this._client.getConfiguration().gatewayConfiguration.assetsUrl;
} else {
this._isDebug = Boolean(options.isDebug);
this._assetsUrl = options.assetsUrl;
}
this._assetsUrl = this._assetsUrl + "/web/" + VERSION;
}
EventEmitter.createChild(BaseFramework);
BaseFramework.prototype._waitForClient = function () {
if (this._client) {
return Promise.resolve();
}
return this._createPromise.then(
function (client) {
this._client = client;
}.bind(this)
);
};
BaseFramework.prototype.setUpEventListeners = function () {
throw new BraintreeError(errors.THREEDS_FRAMEWORK_METHOD_NOT_IMPLEMENTED);
};
BaseFramework.prototype.verifyCard = function (options, privateOptions) {
var formattedOptions, error;
var self = this;
privateOptions = privateOptions || {};
error = this._checkForVerifyCardError(options, privateOptions);
if (error) {
return Promise.reject(error);
}
this._verifyCardInProgress = true;
formattedOptions = this._formatVerifyCardOptions(options);
return this._formatLookupData(formattedOptions)
.then(function (data) {
analytics.sendEvent(
self._createPromise,
"three-d-secure.verification-flow.started"
);
return self._performLookup(formattedOptions.nonce, data);
})
.then(function (response) {
analytics.sendEvent(
self._createPromise,
"three-d-secure.verification-flow.3ds-version." +
response.lookup.threeDSecureVersion
);
return self._onLookupComplete(response, formattedOptions);
})
.then(function (response) {
return self.initializeChallengeWithLookupResponse(
response,
formattedOptions
);
})
.then(function (payload) {
self._resetVerificationState();
analytics.sendEvent(
self._createPromise,
"three-d-secure.verification-flow.completed"
);
return payload;
})
.catch(function (err) {
self._resetVerificationState();
analytics.sendEvent(
self._createPromise,
"three-d-secure.verification-flow.failed"
);
return Promise.reject(err);
});
};
BaseFramework.prototype._checkForFrameworkSpecificVerifyCardErrors =
function () {
throw new BraintreeError(errors.THREEDS_FRAMEWORK_METHOD_NOT_IMPLEMENTED);
};
BaseFramework.prototype._presentChallenge = function () {
throw new BraintreeError(errors.THREEDS_FRAMEWORK_METHOD_NOT_IMPLEMENTED);
};
BaseFramework.prototype.prepareLookup = function () {
throw new BraintreeError(errors.THREEDS_FRAMEWORK_METHOD_NOT_IMPLEMENTED);
};
BaseFramework.prototype._resetVerificationState = function () {
this._verifyCardInProgress = false;
this._verifyCardPromisePlus = null;
if (typeof this._reloadThreeDSecure === "function") {
this._reloadThreeDSecure();
}
};
BaseFramework.prototype._performLookup = function (nonce, data) {
var self = this;
var url = "payment_methods/" + nonce + "/three_d_secure/lookup";
return this._waitForClient().then(function () {
return self._client
.request({
endpoint: url,
method: "post",
data: data,
})
.catch(function (err) {
var status = err && err.details && err.details.httpStatus;
var analyticsMessage = "three-d-secure.verification-flow.lookup-failed";
var lookupError;
if (status === 404) {
lookupError = errors.THREEDS_LOOKUP_TOKENIZED_CARD_NOT_FOUND_ERROR;
analyticsMessage += ".404";
} else if (status === 422) {
lookupError = errors.THREEDS_LOOKUP_VALIDATION_ERROR;
analyticsMessage += ".422";
} else {
lookupError = errors.THREEDS_LOOKUP_ERROR;
}
analytics.sendEvent(self._createPromise, analyticsMessage);
return Promise.reject(
new BraintreeError({
type: lookupError.type,
code: lookupError.code,
message: lookupError.message,
details: {
originalError: err,
},
})
);
});
});
};
BaseFramework.prototype._existsAndIsNumeric = function (value) {
/**
* Returns true if value is numeric and false if value is:
* - undefined
* - null
* - an array
* - an empty string
* - boolean
* - otherwise non-numeric value (e.g. {}, "cows")
*/
return !(
value === undefined || // eslint-disable-line no-undefined
value === null ||
Array.isArray(value) ||
typeof value === "boolean" ||
(typeof value === "string" && value.trim() === "") ||
isNaN(Number(value))
);
};
BaseFramework.prototype._checkForVerifyCardError = function (
options,
privateOptions
) {
var errorOption;
if (this._verifyCardInProgress === true) {
return new BraintreeError(errors.THREEDS_AUTHENTICATION_IN_PROGRESS);
} else if (!options.nonce) {
errorOption = "a nonce";
} else if (
// eslint-disable-next-line no-undefined
!this._existsAndIsNumeric(options.amount)
) {
errorOption = "an amount";
}
if (!errorOption) {
errorOption = this._checkForFrameworkSpecificVerifyCardErrors(
options,
privateOptions
);
}
if (errorOption) {
return new BraintreeError({
type: errors.THREEDS_MISSING_VERIFY_CARD_OPTION.type,
code: errors.THREEDS_MISSING_VERIFY_CARD_OPTION.code,
message: "verifyCard options must include " + errorOption + ".",
});
}
return null;
};
BaseFramework.prototype.initializeChallengeWithLookupResponse = function (
lookupResponse,
options
) {
var self = this;
options = options || {};
this._lookupPaymentMethod = lookupResponse.paymentMethod;
// sets this in the case that initializeChallengeWithLookupResponse is
// called as a standalone method from a server side lookup. In a normal
// verifyCard flow, this promise will already exist
self._verifyCardPromisePlus =
self._verifyCardPromisePlus || new ExtendedPromise();
self._handleLookupResponse(lookupResponse, options);
return self._verifyCardPromisePlus.then(function (payload) {
analytics.sendEvent(
self._createPromise,
"three-d-secure.verification-flow.liability-shifted." +
String(payload.liabilityShifted)
);
analytics.sendEvent(
self._createPromise,
"three-d-secure.verification-flow.liability-shift-possible." +
String(payload.liabilityShiftPossible)
);
return payload;
});
};
BaseFramework.prototype._handleLookupResponse = function (
lookupResponse,
options
) {
var challengeShouldBePresented = Boolean(
lookupResponse.lookup && lookupResponse.lookup.acsUrl
);
var details;
analytics.sendEvent(
this._createPromise,
"three-d-secure.verification-flow.challenge-presented." +
String(challengeShouldBePresented)
);
if (challengeShouldBePresented) {
this._presentChallenge(lookupResponse, options);
} else {
details = this._formatAuthResponse(
lookupResponse.paymentMethod,
lookupResponse.threeDSecureInfo
);
details.verificationDetails = lookupResponse.threeDSecureInfo;
this._verifyCardPromisePlus.resolve(details);
}
};
BaseFramework.prototype._onLookupComplete = function (response) {
this._lookupPaymentMethod = response.paymentMethod;
this._verifyCardPromisePlus = new ExtendedPromise();
return Promise.resolve(response);
};
BaseFramework.prototype._formatAuthResponse = function (
paymentMethod,
threeDSecureInfo
) {
return {
nonce: paymentMethod.nonce,
type: paymentMethod.type,
binData: paymentMethod.binData,
details: paymentMethod.details,
description:
paymentMethod.description &&
paymentMethod.description.replace(/\+/g, " "),
liabilityShifted: threeDSecureInfo && threeDSecureInfo.liabilityShifted,
liabilityShiftPossible:
threeDSecureInfo && threeDSecureInfo.liabilityShiftPossible,
threeDSecureInfo: paymentMethod.threeDSecureInfo,
};
};
BaseFramework.prototype._formatVerifyCardOptions = function (options) {
return assign({}, options);
};
BaseFramework.prototype._formatLookupData = function (options) {
var data = {
amount: options.amount,
};
if (options.collectDeviceData === true) {
data.browserColorDepth = window.screen.colorDepth;
data.browserJavaEnabled = window.navigator.javaEnabled();
data.browserJavascriptEnabled = true;
data.browserLanguage = window.navigator.language;
data.browserScreenHeight = window.screen.height;
data.browserScreenWidth = window.screen.width;
data.browserTimeZone = new Date().getTimezoneOffset();
data.deviceChannel = "Browser";
}
return Promise.resolve(data);
};
BaseFramework.prototype._handleV1AuthResponse = function (data) {
var authResponse = JSON.parse(data.auth_response);
if (authResponse.success) {
this._verifyCardPromisePlus.resolve(
this._formatAuthResponse(
authResponse.paymentMethod,
authResponse.threeDSecureInfo
)
);
} else if (
authResponse.threeDSecureInfo &&
authResponse.threeDSecureInfo.liabilityShiftPossible
) {
this._verifyCardPromisePlus.resolve(
this._formatAuthResponse(
this._lookupPaymentMethod,
authResponse.threeDSecureInfo
)
);
} else {
this._verifyCardPromisePlus.reject(
new BraintreeError({
type: BraintreeError.types.UNKNOWN,
code: "UNKNOWN_AUTH_RESPONSE",
message: authResponse.error.message,
})
);
}
};
BaseFramework.prototype.cancelVerifyCard = function () {
var response, threeDSecureInfo;
this._verifyCardInProgress = false;
if (!this._lookupPaymentMethod) {
return Promise.reject(
new BraintreeError(errors.THREEDS_NO_VERIFICATION_PAYLOAD)
);
}
threeDSecureInfo = this._lookupPaymentMethod.threeDSecureInfo;
response = assign({}, this._lookupPaymentMethod, {
liabilityShiftPossible:
threeDSecureInfo && threeDSecureInfo.liabilityShiftPossible,
liabilityShifted: threeDSecureInfo && threeDSecureInfo.liabilityShifted,
verificationDetails:
threeDSecureInfo && threeDSecureInfo.verificationDetails,
});
return Promise.resolve(response);
};
BaseFramework.prototype._setupV1Bus = function (options) {
var clientConfiguration = this._client.getConfiguration();
var parentURL = window.location.href.split("#")[0];
var lookupResponse = options.lookupResponse;
var channel = uuid();
var bus = new Bus({
channel: channel,
verifyDomain: isVerifiedDomain,
});
var authenticationCompleteBaseUrl =
this._assetsUrl +
"/html/three-d-secure-authentication-complete-frame.html?channel=" +
encodeURIComponent(channel) +
"&";
bus.on(BUS_CONFIGURATION_REQUEST_EVENT, function (reply) {
reply({
clientConfiguration: clientConfiguration,
nonce: options.nonce,
acsUrl: lookupResponse.acsUrl,
pareq: lookupResponse.pareq,
termUrl:
lookupResponse.termUrl +
"&three_d_secure_version=" +
VERSION +
"&authentication_complete_base_url=" +
encodeURIComponent(authenticationCompleteBaseUrl),
md: lookupResponse.md,
parentUrl: parentURL,
});
});
bus.on(events.AUTHENTICATION_COMPLETE, options.handleAuthResponse);
return bus;
};
BaseFramework.prototype._setupV1Iframe = function (options) {
var url =
this._assetsUrl +
"/html/three-d-secure-bank-frame" +
useMin(this._isDebug) +
".html?showLoader=" +
options.showLoader;
var bankIframe = iFramer({
src: url,
height: IFRAME_HEIGHT,
width: IFRAME_WIDTH,
name: constants.LANDING_FRAME_NAME + "_" + this._v1Bus.channel,
title: "3D Secure Authorization Frame",
});
return bankIframe;
};
BaseFramework.prototype._setupV1Elements = function (options) {
this._v1Bus = this._setupV1Bus(options);
this._v1Iframe = this._setupV1Iframe(options);
};
BaseFramework.prototype._teardownV1Elements = function () {
if (this._v1Bus) {
this._v1Bus.teardown();
this._v1Bus = null;
}
if (this._v1Iframe && this._v1Iframe.parentNode) {
this._v1Iframe.parentNode.removeChild(this._v1Iframe);
this._v1Iframe = null;
}
if (this._onV1Keyup) {
document.removeEventListener("keyup", this._onV1Keyup);
this._onV1Keyup = null;
}
};
BaseFramework.prototype.teardown = function () {
analytics.sendEvent(this._createPromise, "three-d-secure.teardown-completed");
this._teardownV1Elements();
return Promise.resolve();
};
module.exports = BaseFramework;