'use strict';
var BRAINTREE_VERSION = require('./constants').BRAINTREE_VERSION;
var GraphQL = require('./request/graphql');
var request = require('./request');
var isVerifiedDomain = require('../lib/is-verified-domain');
var BraintreeError = require('../lib/braintree-error');
var convertToBraintreeError = require('../lib/convert-to-braintree-error');
var createAuthorizationData = require('../lib/create-authorization-data');
var getGatewayConfiguration = require('./get-configuration').getConfiguration;
var addMetadata = require('../lib/add-metadata');
var Promise = require('../lib/promise');
var wrapPromise = require('@braintree/wrap-promise');
var once = require('../lib/once');
var deferred = require('../lib/deferred');
var assign = require('../lib/assign').assign;
var analytics = require('../lib/analytics');
var constants = require('./constants');
var errors = require('./errors');
var sharedErrors = require('../lib/errors');
var VERSION = require('../lib/constants').VERSION;
var GRAPHQL_URLS = require('../lib/constants').GRAPHQL_URLS;
var methods = require('../lib/methods');
var convertMethodsToError = require('../lib/convert-methods-to-error');
var assets = require('../lib/assets');
var FRAUDNET_FNCLS = require('../lib/constants').FRAUDNET_FNCLS;
var FRAUDNET_SOURCE = require('../lib/constants').FRAUDNET_SOURCE;
var FRAUDNET_URL = require('../lib/constants').FRAUDNET_URL;
var cachedClients = {};
function Client(configuration) {
var configurationJSON, gatewayConfiguration, braintreeApiConfiguration;
configuration = configuration || {};
configurationJSON = JSON.stringify(configuration);
gatewayConfiguration = configuration.gatewayConfiguration;
if (!gatewayConfiguration) {
throw new BraintreeError(errors.CLIENT_MISSING_GATEWAY_CONFIGURATION);
}
[
'assetsUrl',
'clientApiUrl',
'configUrl'
].forEach(function (property) {
if (property in gatewayConfiguration && !isVerifiedDomain(gatewayConfiguration[property])) {
throw new BraintreeError({
type: errors.CLIENT_GATEWAY_CONFIGURATION_INVALID_DOMAIN.type,
code: errors.CLIENT_GATEWAY_CONFIGURATION_INVALID_DOMAIN.code,
message: property + ' property is on an invalid domain.'
});
}
});
this.getConfiguration = function () {
return JSON.parse(configurationJSON);
};
this._request = request;
this._configuration = this.getConfiguration();
this._clientApiBaseUrl = gatewayConfiguration.clientApiUrl + '/v1/';
braintreeApiConfiguration = gatewayConfiguration.braintreeApi;
if (braintreeApiConfiguration) {
this._braintreeApi = {
baseUrl: braintreeApiConfiguration.url + '/',
accessToken: braintreeApiConfiguration.accessToken
};
if (!isVerifiedDomain(this._braintreeApi.baseUrl)) {
throw new BraintreeError({
type: errors.CLIENT_GATEWAY_CONFIGURATION_INVALID_DOMAIN.type,
code: errors.CLIENT_GATEWAY_CONFIGURATION_INVALID_DOMAIN.code,
message: 'braintreeApi URL is on an invalid domain.'
});
}
}
if (gatewayConfiguration.graphQL) {
this._graphQL = new GraphQL({
graphQL: gatewayConfiguration.graphQL
});
}
}
Client.initialize = function (options) {
var clientInstance;
var promise = cachedClients[options.authorization];
if (promise) {
analytics.sendEvent(promise, 'custom.client.load.cached');
return promise;
}
promise = getGatewayConfiguration(options).then(function (configuration) {
if (options.debug) {
configuration.isDebug = true;
}
clientInstance = new Client(configuration);
return clientInstance;
});
cachedClients[options.authorization] = promise;
analytics.sendEvent(promise, 'custom.client.load.initialized');
return promise.then(function (client) {
analytics.sendEvent(clientInstance, 'custom.client.load.succeeded');
return client;
}).catch(function (err) {
delete cachedClients[options.authorization];
return Promise.reject(err);
});
};
Client.clearCache = function () {
cachedClients = {};
};
Client.prototype._findOrCreateFraudnetJSON = function (clientMetadataId) {
var el = document.querySelector('script[fncls="' + FRAUDNET_FNCLS + '"]');
var config, additionalData, authorizationFingerprint, parameters;
if (!el) {
el = document.body.appendChild(document.createElement('script'));
el.type = 'application/json';
el.setAttribute('fncls', FRAUDNET_FNCLS);
}
config = this.getConfiguration();
additionalData = {
rda_tenant: 'bt_card',
mid: config.gatewayConfiguration.merchantId
};
authorizationFingerprint = createAuthorizationData(config.authorization).attrs.authorizationFingerprint;
if (authorizationFingerprint) {
authorizationFingerprint.split('&').forEach(function (pieces) {
var component = pieces.split('=');
if (component[0] === 'customer_id' && component.length > 1) {
additionalData.cid = component[1];
}
});
}
parameters = {
f: clientMetadataId.substr(0, 32),
fp: additionalData,
bu: false,
s: FRAUDNET_SOURCE
};
el.text = JSON.stringify(parameters);
};
Client.prototype.request = function (options, callback) {
var self = this;
var requestPromise = new Promise(function (resolve, reject) {
var optionName, api, baseUrl, requestOptions;
var shouldCollectData = Boolean(options.endpoint === 'payment_methods/credit_cards' && self.getConfiguration().gatewayConfiguration.creditCards.collectDeviceData);
if (options.api !== 'graphQLApi') {
if (!options.method) {
optionName = 'options.method';
} else if (!options.endpoint) {
optionName = 'options.endpoint';
}
}
if (optionName) {
throw new BraintreeError({
type: errors.CLIENT_OPTION_REQUIRED.type,
code: errors.CLIENT_OPTION_REQUIRED.code,
message: optionName + ' is required when making a request.'
});
}
if ('api' in options) {
api = options.api;
} else {
api = 'clientApi';
}
requestOptions = {
method: options.method,
graphQL: self._graphQL,
timeout: options.timeout,
metadata: self._configuration.analyticsMetadata
};
if (api === 'clientApi') {
baseUrl = self._clientApiBaseUrl;
requestOptions.data = addMetadata(self._configuration, options.data);
} else if (api === 'braintreeApi') {
if (!self._braintreeApi) {
throw new BraintreeError(sharedErrors.BRAINTREE_API_ACCESS_RESTRICTED);
}
baseUrl = self._braintreeApi.baseUrl;
requestOptions.data = options.data;
requestOptions.headers = {
'Braintree-Version': constants.BRAINTREE_API_VERSION_HEADER,
Authorization: 'Bearer ' + self._braintreeApi.accessToken
};
} else if (api === 'graphQLApi') {
baseUrl = GRAPHQL_URLS[self._configuration.gatewayConfiguration.environment];
options.endpoint = '';
requestOptions.method = 'post';
requestOptions.data = assign({
clientSdkMetadata: {
source: self._configuration.analyticsMetadata.source,
integration: self._configuration.analyticsMetadata.integration,
sessionId: self._configuration.analyticsMetadata.sessionId
}
}, options.data);
requestOptions.headers = getAuthorizationHeadersForGraphQL(self._configuration.authorization);
} else {
throw new BraintreeError({
type: errors.CLIENT_OPTION_INVALID.type,
code: errors.CLIENT_OPTION_INVALID.code,
message: 'options.api is invalid.'
});
}
requestOptions.url = baseUrl + options.endpoint;
requestOptions.sendAnalyticsEvent = function (kind) {
analytics.sendEvent(self, kind);
};
self._request(requestOptions, function (err, data, status) {
var resolvedData, requestError;
requestError = formatRequestError(status, err);
if (requestError) {
reject(requestError);
return;
}
if (api === 'graphQLApi' && data.errors) {
reject(convertToBraintreeError(data.errors, {
type: errors.CLIENT_GRAPHQL_REQUEST_ERROR.type,
code: errors.CLIENT_GRAPHQL_REQUEST_ERROR.code,
message: errors.CLIENT_GRAPHQL_REQUEST_ERROR.message
}));
return;
}
resolvedData = assign({_httpStatus: status}, data);
if (shouldCollectData && resolvedData.creditCards && resolvedData.creditCards.length > 0) {
self._findOrCreateFraudnetJSON(resolvedData.creditCards[0].nonce);
assets.loadScript({
src: FRAUDNET_URL,
forceScriptReload: true
});
}
resolve(resolvedData);
});
});
if (typeof callback === 'function') {
callback = once(deferred(callback));
requestPromise.then(function (response) {
callback(null, response, response._httpStatus);
}).catch(function (err) {
var status = err && err.details && err.details.httpStatus;
callback(err, null, status);
});
return;
}
return requestPromise;
};
function formatRequestError(status, err) {
var requestError;
if (status === -1) {
requestError = new BraintreeError(errors.CLIENT_REQUEST_TIMEOUT);
} else if (status === 403) {
requestError = new BraintreeError(errors.CLIENT_AUTHORIZATION_INSUFFICIENT);
} else if (status === 429) {
requestError = new BraintreeError(errors.CLIENT_RATE_LIMITED);
} else if (status >= 500) {
requestError = new BraintreeError(errors.CLIENT_GATEWAY_NETWORK);
} else if (status < 200 || status >= 400) {
requestError = convertToBraintreeError(err, {
type: errors.CLIENT_REQUEST_ERROR.type,
code: errors.CLIENT_REQUEST_ERROR.code,
message: errors.CLIENT_REQUEST_ERROR.message
});
}
if (requestError) {
requestError.details = requestError.details || {};
requestError.details.httpStatus = status;
return requestError;
}
}
Client.prototype.toJSON = function () {
return this.getConfiguration();
};
Client.prototype.getVersion = function () {
return VERSION;
};
Client.prototype.teardown = wrapPromise(function () {
var self = this;
delete cachedClients[self.getConfiguration().authorization];
convertMethodsToError(self, methods(Client.prototype));
return Promise.resolve();
});
function getAuthorizationHeadersForGraphQL(authorization) {
var authAttrs = createAuthorizationData(authorization).attrs;
var token = authAttrs.authorizationFingerprint || authAttrs.tokenizationKey;
return {
Authorization: 'Bearer ' + token,
'Braintree-Version': BRAINTREE_VERSION
};
}
module.exports = Client;