'use strict';
var BraintreeError = require('../lib/braintree-error');
var constants = require('./constants');
var errors = require('./errors');
var sharedErrors = require('../lib/errors');
var analytics = require('../lib/analytics');
var once = require('../lib/once');
var convertMethodsToError = require('../lib/convert-methods-to-error');
var methods = require('../lib/methods');
var Promise = require('../lib/promise');
var wrapPromise = require('@braintree/wrap-promise');
var TOKENIZE_BANK_DETAILS_MUTATION = createGraphQLMutation('UsBankAccount');
var TOKENIZE_BANK_LOGIN_MUTATION = createGraphQLMutation('UsBankLogin');
/**
* @typedef {object} USBankAccount~tokenizePayload
* @property {string} nonce The payment method nonce.
* @property {string} type The payment method type, always `us_bank_account`.
* @property {object} details Additional account details. Currently empty.
*/
/**
* @class
* @param {object} options See {@link module:braintree-web/us-bank-account.create|us-bank-account.create}.
* @classdesc This class represents a US Bank Account component. Instances of this class can tokenize raw bank details or present a bank login. <strong>You cannot use this constructor directly. Use {@link module:braintree-web/us-bank-account.create|braintree.us-bank-account.create} instead.</strong>
*/
function USBankAccount(options) {
this._client = options.client;
this._isTokenizingBankLogin = false;
analytics.sendEvent(this._client, 'usbankaccount.initialized');
}
/**
* Tokenizes bank information to return a payment method nonce. You can tokenize bank details by providing information like account and routing numbers. You can also tokenize with a bank login UI that prompts the customer to log into their bank account.
* @public
* @param {object} options All tokenization options for the US Bank Account component.
* @param {string} options.mandateText A string for proof of customer authorization. For example, `'I authorize Braintree to debit my bank account on behalf of My Online Store.'`.
* @param {object} [options.bankDetails] Bank detail information (such as account and routing numbers). `bankDetails` or `bankLogin` option must be provided.
* @param {string} options.bankDetails.routingNumber The customer's bank routing number, such as `'307075259'`.
* @param {string} options.bankDetails.accountNumber The customer's bank account number, such as `'999999999'`.
* @param {string} options.bankDetails.accountType The customer's bank account type. Must be `'checking'` or `'savings'`.
* @param {string} options.bankDetails.ownershipType The customer's bank account ownership type. Must be `'personal'` or `'business'`.
* @param {string} [options.bankDetails.firstName] The customer's first name. Required when account ownership type is `personal`.
* @param {string} [options.bankDetails.lastName] The customer's last name. Required when account ownership type is `personal`.
* @param {string} [options.bankDetails.businessName] The customer's business name. Required when account ownership type is `business`.
* @param {object} options.bankDetails.billingAddress The customer's billing address.
* @param {string} options.bankDetails.billingAddress.streetAddress The street address for the customer's billing address, such as `'123 Fake St'`.
* @param {string} [options.bankDetails.billingAddress.extendedAddress] The extended street address for the customer's billing address, such as `'Apartment B'`.
* @param {string} options.bankDetails.billingAddress.locality The locality for the customer's billing address. This is typically a city, such as `'San Francisco'`.
* @param {string} options.bankDetails.billingAddress.region The region for the customer's billing address. This is typically a state, such as `'CA'`.
* @param {string} options.bankDetails.billingAddress.postalCode The postal code for the customer's billing address. This is typically a ZIP code, such as `'94119'`.
* @param {object} [options.bankLogin] Bank login information. `bankLogin` or `bankDetails` option must be provided.
* @param {string} options.bankLogin.displayName Display name for the bank login UI, such as `'My Store'`.
* @param {string} options.bankLogin.ownershipType The customer's bank account ownership type. Must be `'personal'` or `'business'`.
* @param {string} [options.bankLogin.firstName] The customer's first name. Required when account ownership type is `personal`.
* @param {string} [options.bankLogin.lastName] The customer's last name. Required when account ownership type is `personal`.
* @param {string} [options.bankLogin.businessName] The customer's business name. Required when account ownership type is `business`.
* @param {object} options.bankLogin.billingAddress The customer's billing address.
* @param {string} options.bankLogin.billingAddress.streetAddress The street address for the customer's billing address, such as `'123 Fake St'`.
* @param {string} [options.bankLogin.billingAddress.extendedAddress] The extended street address for the customer's billing address, such as `'Apartment B'`.
* @param {string} options.bankLogin.billingAddress.locality The locality for the customer's billing address. This is typically a city, such as `'San Francisco'`.
* @param {string} options.bankLogin.billingAddress.region The region for the customer's billing address. This is typically a state, such as `'CA'`.
* @param {string} options.bankLogin.billingAddress.postalCode The postal code for the customer's billing address. This is typically a ZIP code, such as `'94119'`.
* @param {callback} [callback] The second argument, <code>data</code>, is a {@link USBankAccount~tokenizePayload|tokenizePayload}. If no callback is provided, `tokenize` returns a promise that resolves with {@link USBankAccount~tokenizePayload|tokenizePayload}.
* @returns {(Promise|void)} Returns a promise if no callback is provided.
* @example
* <caption>Tokenizing raw bank details</caption>
* var routingNumberInput = document.querySelector('input[name="routing-number"]');
* var accountNumberInput = document.querySelector('input[name="account-number"]');
* var accountTypeInput = document.querySelector('input[name="account-type"]:checked');
* var ownershipTypeInput = document.querySelector('input[name="ownership-type"]:checked');
* var firstNameInput = document.querySelector('input[name="first-name"]');
* var lastNameInput = document.querySelector('input[name="last-name"]');
* var businessNameInput = document.querySelector('input[name="business-name"]');
* var billingAddressStreetInput = document.querySelector('input[name="street-address"]');
* var billingAddressExtendedInput = document.querySelector('input[name="extended-address"]');
* var billingAddressLocalityInput = document.querySelector('input[name="locality"]');
* var billingAddressRegionSelect = document.querySelector('select[name="region"]');
* var billingAddressPostalInput = document.querySelector('input[name="postal-code"]');
*
* submitButton.addEventListener('click', function (event) {
* var bankDetails = {
* routingNumber: routingNumberInput.value,
* accountNumber: accountNumberInput.value,
* accountType: accountTypeInput.value,
* ownershipType: ownershipTypeInput.value,
* billingAddress: {
* streetAddress: billingAddressStreetInput.value,
* extendedAddress: billingAddressExtendedInput.value,
* locality: billingAddressLocalityInput.value,
* region: billingAddressRegionSelect.value,
* postalCode: billingAddressPostalInput.value
* }
* };
*
* if (bankDetails.ownershipType === 'personal') {
* bankDetails.firstName = firstNameInput.value;
* bankDetails.lastName = lastNameInput.value;
* } else {
* bankDetails.businessName = businessNameInput.value;
* }
*
* event.preventDefault();
*
* usBankAccountInstance.tokenize({
* bankDetails: bankDetails,
* mandateText: 'I authorize Braintree to debit my bank account on behalf of My Online Store.'
* }, function (tokenizeErr, tokenizedPayload) {
* if (tokenizeErr) {
* console.error('There was an error tokenizing the bank details.');
* return;
* }
*
* // Send tokenizePayload.nonce to your server here!
* });
* });
* @example
* <caption>Tokenizing with bank login UI</caption>
* var ownershipTypeInput = document.querySelector('input[name="ownership-type"]:checked');
* var firstNameInput = document.querySelector('input[name="first-name"]');
* var lastNameInput = document.querySelector('input[name="last-name"]');
* var businessNameInput = document.querySelector('input[name="business-name"]');
* var billingAddressStreetInput = document.querySelector('input[name="street-address"]');
* var billingAddressExtendedInput = document.querySelector('input[name="extended-address"]');
* var billingAddressLocalityInput = document.querySelector('input[name="locality"]');
* var billingAddressRegionSelect = document.querySelector('select[name="region"]');
* var billingAddressPostalInput = document.querySelector('input[name="postal-code"]');
*
* bankLoginButton.addEventListener('click', function (event) {
* var bankLogin = {
* displayName: 'My Online Store',
* ownershipType: ownershipTypeInput.value,
* billingAddress: {
* streetAddress: billingAddressStreetInput.value,
* extendedAddress: billingAddressExtendedInput.value,
* locality: billingAddressLocalityInput.value,
* region: billingAddressRegionSelect.value,
* postalCode: billingAddressPostalInput.value
* }
* }
* event.preventDefault();
*
* if (bankLogin.ownershipType === 'personal') {
* bankLogin.firstName = firstNameInput.value;
* bankLogin.lastName = lastNameInput.value;
* } else {
* bankLogin.businessName = businessNameInput.value;
* }
*
* usBankAccountInstance.tokenize({
* bankLogin: bankLogin,
* mandateText: 'I authorize Braintree to debit my bank account on behalf of My Online Store.'
* }, function (tokenizeErr, tokenizedPayload) {
* if (tokenizeErr) {
* console.error('There was an error tokenizing the bank details.');
* return;
* }
*
* // Send tokenizePayload.nonce to your server here!
* });
* });
*/
USBankAccount.prototype.tokenize = function (options) {
options = options || {};
if (!options.mandateText) {
return Promise.reject(new BraintreeError({
type: errors.US_BANK_ACCOUNT_OPTION_REQUIRED.type,
code: errors.US_BANK_ACCOUNT_OPTION_REQUIRED.code,
message: 'mandateText property is required.'
}));
}
if (options.bankDetails && options.bankLogin) {
return Promise.reject(new BraintreeError({
type: errors.US_BANK_ACCOUNT_MUTUALLY_EXCLUSIVE_OPTIONS.type,
code: errors.US_BANK_ACCOUNT_MUTUALLY_EXCLUSIVE_OPTIONS.code,
message: 'tokenize must be called with bankDetails or bankLogin, not both.'
}));
} else if (options.bankDetails) {
return this._tokenizeBankDetails(options);
} else if (options.bankLogin) {
return this._tokenizeBankLogin(options);
}
return Promise.reject(new BraintreeError({
type: errors.US_BANK_ACCOUNT_OPTION_REQUIRED.type,
code: errors.US_BANK_ACCOUNT_OPTION_REQUIRED.code,
message: 'tokenize must be called with bankDetails or bankLogin.'
}));
};
USBankAccount.prototype._tokenizeBankDetails = function (options) {
var client = this._client;
var bankDetails = options.bankDetails;
var data = {
achMandate: options.mandateText,
routingNumber: bankDetails.routingNumber,
accountNumber: bankDetails.accountNumber,
accountType: bankDetails.accountType.toUpperCase(),
billingAddress: formatBillingAddressForGraphQL(bankDetails.billingAddress || {})
};
formatDataForOwnershipType(data, bankDetails);
return client.request({
api: 'graphQLApi',
data: {
query: TOKENIZE_BANK_DETAILS_MUTATION,
variables: {
input: {
usBankAccount: data
}
}
}
}).then(function (response) {
analytics.sendEvent(client, 'usbankaccount.bankdetails.tokenization.succeeded');
return Promise.resolve(formatTokenizeResponseFromGraphQL(response, 'tokenizeUsBankAccount'));
}).catch(function (err) {
var error = errorFrom(err);
analytics.sendEvent(client, 'usbankaccount.bankdetails.tokenization.failed');
return Promise.reject(error);
});
};
USBankAccount.prototype._tokenizeBankLogin = function (options) {
var self = this;
var client = this._client;
var gatewayConfiguration = client.getConfiguration().gatewayConfiguration;
var isProduction = gatewayConfiguration.environment === 'production';
var plaidConfig = gatewayConfiguration.usBankAccount.plaid;
if (!options.bankLogin.displayName) {
return Promise.reject(new BraintreeError({
type: errors.US_BANK_ACCOUNT_OPTION_REQUIRED.type,
code: errors.US_BANK_ACCOUNT_OPTION_REQUIRED.code,
message: 'displayName property is required when using bankLogin.'
}));
}
if (!plaidConfig) {
return Promise.reject(new BraintreeError(errors.US_BANK_ACCOUNT_BANK_LOGIN_NOT_ENABLED));
}
if (this._isTokenizingBankLogin) {
return Promise.reject(new BraintreeError(errors.US_BANK_ACCOUNT_LOGIN_REQUEST_ACTIVE));
}
this._isTokenizingBankLogin = true;
return new Promise(function (resolve, reject) {
self._loadPlaid(function (plaidLoadErr, plaid) {
if (plaidLoadErr) {
reject(plaidLoadErr);
return;
}
plaid.create({
clientName: options.bankLogin.displayName,
apiVersion: 'v2',
env: isProduction ? 'production' : 'sandbox',
key: plaidConfig.publicKey,
product: 'auth',
selectAccount: true,
onExit: function () {
self._isTokenizingBankLogin = false;
analytics.sendEvent(client, 'usbankaccount.banklogin.tokenization.closed.by-user');
reject(new BraintreeError(errors.US_BANK_ACCOUNT_LOGIN_CLOSED));
},
onSuccess: function (publicToken, metadata) {
var bankLogin = options.bankLogin;
var data = {
publicToken: publicToken,
accountId: isProduction ? metadata.account_id : 'plaid_account_id',
accountType: metadata.account.subtype.toUpperCase(),
achMandate: options.mandateText,
billingAddress: formatBillingAddressForGraphQL(bankLogin.billingAddress || {})
};
formatDataForOwnershipType(data, bankLogin);
client.request({
api: 'graphQLApi',
data: {
query: TOKENIZE_BANK_LOGIN_MUTATION,
variables: {
input: {
usBankLogin: data
}
}
}
}).then(function (response) {
self._isTokenizingBankLogin = false;
analytics.sendEvent(client, 'usbankaccount.banklogin.tokenization.succeeded');
resolve(formatTokenizeResponseFromGraphQL(response, 'tokenizeUsBankLogin'));
}).catch(function (tokenizeErr) {
var error;
self._isTokenizingBankLogin = false;
error = errorFrom(tokenizeErr);
analytics.sendEvent(client, 'usbankaccount.banklogin.tokenization.failed');
reject(error);
});
}
}).open();
analytics.sendEvent(client, 'usbankaccount.banklogin.tokenization.started');
});
});
};
function errorFrom(err) {
var error;
var status = err.details && err.details.httpStatus;
if (status === 401) {
error = new BraintreeError(sharedErrors.BRAINTREE_API_ACCESS_RESTRICTED);
} else if (status < 500) {
error = new BraintreeError(errors.US_BANK_ACCOUNT_FAILED_TOKENIZATION);
} else {
error = new BraintreeError(errors.US_BANK_ACCOUNT_TOKENIZATION_NETWORK_ERROR);
}
error.details = {originalError: err};
return error;
}
function formatTokenizeResponseFromGraphQL(response, type) {
var data = response.data[type].paymentMethod;
var last4 = data.details.last4;
var description = 'US bank account ending in - ' + last4;
return {
nonce: data.id,
details: {},
description: description,
type: 'us_bank_account'
};
}
USBankAccount.prototype._loadPlaid = function (callback) {
var existingScript, script;
callback = once(callback);
if (window.Plaid) {
callback(null, window.Plaid);
return;
}
existingScript = document.querySelector('script[src="' + constants.PLAID_LINK_JS + '"]');
if (existingScript) {
addLoadListeners(existingScript, callback);
} else {
script = document.createElement('script');
script.src = constants.PLAID_LINK_JS;
script.async = true;
addLoadListeners(script, callback);
document.body.appendChild(script);
this._plaidScript = script;
}
};
function addLoadListeners(script, callback) {
function loadHandler() {
var readyState = this.readyState; // eslint-disable-line no-invalid-this
if (!readyState || readyState === 'loaded' || readyState === 'complete') {
removeLoadListeners();
callback(null, window.Plaid);
}
}
function errorHandler() {
script.parentNode.removeChild(script);
callback(new BraintreeError(errors.US_BANK_ACCOUNT_LOGIN_LOAD_FAILED));
}
function removeLoadListeners() {
script.removeEventListener('error', errorHandler);
script.removeEventListener('load', loadHandler);
script.removeEventListener('readystatechange', loadHandler);
}
script.addEventListener('error', errorHandler);
script.addEventListener('load', loadHandler);
script.addEventListener('readystatechange', loadHandler);
}
function formatBillingAddressForGraphQL(address) {
return {
streetAddress: address.streetAddress,
extendedAddress: address.extendedAddress,
city: address.locality,
state: address.region,
zipCode: address.postalCode
};
}
function formatDataForOwnershipType(data, details) {
if (details.ownershipType === 'personal') {
data.individualOwner = {
firstName: details.firstName,
lastName: details.lastName
};
} else if (details.ownershipType === 'business') {
data.businessOwner = {
businessName: details.businessName
};
}
}
function createGraphQLMutation(type) {
return '' +
'mutation Tokenize' + type + '($input: Tokenize' + type + 'Input!) {' +
' tokenize' + type + '(input: $input) {' +
' paymentMethod {' +
' id' +
' details {' +
' ... on UsBankAccountDetails {' +
' last4' +
' }' +
' }' +
' }' +
' }' +
'}';
}
/**
* Cleanly tear down anything set up by {@link module:braintree-web/us-bank-account.create|create}.
* @public
* @param {callback} [callback] Called once teardown is complete. No data is returned if teardown completes successfully.
* @example
* usBankAccountInstance.teardown();
* @example <caption>With callback</caption>
* usBankAccountInstance.teardown(function () {
* // teardown is complete
* });
* @returns {(Promise|void)} Returns a promise if no callback is provided.
*/
USBankAccount.prototype.teardown = function () {
if (this._plaidScript) {
document.body.removeChild(this._plaidScript);
}
convertMethodsToError(this, methods(USBankAccount.prototype));
return Promise.resolve();
};
module.exports = wrapPromise.wrapPrototype(USBankAccount);