'use strict';
var assign = require('../../lib/assign').assign;
var createAssetsUrl = require('../../lib/create-assets-url');
var Destructor = require('../../lib/destructor');
var classList = require('@braintree/class-list');
var iFramer = require('@braintree/iframer');
var Bus = require('../../lib/bus');
var createDeferredClient = require('../../lib/create-deferred-client');
var BraintreeError = require('../../lib/braintree-error');
var composeUrl = require('./compose-url');
var getStylesFromClass = require('./get-styles-from-class');
var constants = require('../shared/constants');
var errors = require('../shared/errors');
var INTEGRATION_TIMEOUT_MS = require('../../lib/constants').INTEGRATION_TIMEOUT_MS;
var uuid = require('../../lib/vendor/uuid');
var findParentTags = require('../shared/find-parent-tags');
var browserDetection = require('../shared/browser-detection');
var events = constants.events;
var EventEmitter = require('../../lib/event-emitter');
var injectFrame = require('./inject-frame');
var analytics = require('../../lib/analytics');
var allowedFields = constants.allowedFields;
var methods = require('../../lib/methods');
var convertMethodsToError = require('../../lib/convert-methods-to-error');
var sharedErrors = require('../../lib/errors');
var getCardTypes = require('../shared/get-card-types');
var attributeValidationError = require('./attribute-validation-error');
var Promise = require('../../lib/promise');
var wrapPromise = require('@braintree/wrap-promise');
/**
* @typedef {object} HostedFields~tokenizePayload
* @property {string} nonce The payment method nonce.
* @property {object} details Additional account details.
* @property {string} details.bin The BIN number of the card.
* @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 {string} type The payment method type, always `CreditCard`.
* @property {object} binData Information about the card based on the bin.
* @property {string} binData.commercial Possible values: 'Yes', 'No', 'Unknown'.
* @property {string} binData.countryOfIssuance The country of issuance.
* @property {string} binData.debit Possible values: 'Yes', 'No', 'Unknown'.
* @property {string} binData.durbinRegulated Possible values: 'Yes', 'No', 'Unknown'.
* @property {string} binData.healthcare Possible values: 'Yes', 'No', 'Unknown'.
* @property {string} binData.issuingBank The issuing bank.
* @property {string} binData.payroll Possible values: 'Yes', 'No', 'Unknown'.
* @property {string} binData.prepaid Possible values: 'Yes', 'No', 'Unknown'.
* @property {string} binData.productId The product id.
*/
/**
* @typedef {object} HostedFields~stateObject
* @description The event payload sent from {@link HostedFields#on|on} or {@link HostedFields#getState|getState}.
* @property {HostedFields~hostedFieldsCard[]} cards
* This will return an array of potential {@link HostedFields~hostedFieldsCard|cards}. If the card type has been determined, the array will contain only one card.
* Internally, Hosted Fields uses <a href="https://github.com/braintree/credit-card-type">credit-card-type</a>,
* an open-source card detection library.
* @property {string} emittedBy
* The name of the field associated with an event. This will not be included if returned by {@link HostedFields#getState|getState}. It will be one of the following strings:<br>
* - `"number"`
* - `"cvv"`
* - `"expirationDate"`
* - `"expirationMonth"`
* - `"expirationYear"`
* - `"postalCode"`
* @property {object} fields
* @property {?HostedFields~hostedFieldsFieldData} fields.number {@link HostedFields~hostedFieldsFieldData|hostedFieldsFieldData} for the number field, if it is present.
* @property {?HostedFields~hostedFieldsFieldData} fields.cvv {@link HostedFields~hostedFieldsFieldData|hostedFieldsFieldData} for the CVV field, if it is present.
* @property {?HostedFields~hostedFieldsFieldData} fields.expirationDate {@link HostedFields~hostedFieldsFieldData|hostedFieldsFieldData} for the expiration date field, if it is present.
* @property {?HostedFields~hostedFieldsFieldData} fields.expirationMonth {@link HostedFields~hostedFieldsFieldData|hostedFieldsFieldData} for the expiration month field, if it is present.
* @property {?HostedFields~hostedFieldsFieldData} fields.expirationYear {@link HostedFields~hostedFieldsFieldData|hostedFieldsFieldData} for the expiration year field, if it is present.
* @property {?HostedFields~hostedFieldsFieldData} fields.postalCode {@link HostedFields~hostedFieldsFieldData|hostedFieldsFieldData} for the postal code field, if it is present.
*/
/**
* @typedef {object} HostedFields~hostedFieldsFieldData
* @description Data about Hosted Fields fields, sent in {@link HostedFields~stateObject|stateObjects}.
* @property {HTMLElement} container Reference to the container DOM element on your page associated with the current event.
* @property {boolean} isFocused Whether or not the input is currently focused.
* @property {boolean} isEmpty Whether or not the user has entered a value in the input.
* @property {boolean} isPotentiallyValid
* A determination based on the future validity of the input value.
* This is helpful when a user is entering a card number and types <code>"41"</code>.
* While that value is not valid for submission, it is still possible for
* it to become a fully qualified entry. However, if the user enters <code>"4x"</code>
* it is clear that the card number can never become valid and isPotentiallyValid will
* return false.
* @property {boolean} isValid Whether or not the value of the associated input is <i>fully</i> qualified for submission.
*/
/**
* @typedef {object} HostedFields~hostedFieldsCard
* @description Information about the card type, sent in {@link HostedFields~stateObject|stateObjects}.
* @property {string} type The code-friendly representation of the card type. It will be one of the following strings:
* - `american-express`
* - `diners-club`
* - `discover`
* - `jcb`
* - `maestro`
* - `master-card`
* - `unionpay`
* - `visa`
* @property {string} niceType The pretty-printed card type. It will be one of the following strings:
* - `American Express`
* - `Diners Club`
* - `Discover`
* - `JCB`
* - `Maestro`
* - `MasterCard`
* - `UnionPay`
* - `Visa`
* @property {object} code
* This object contains data relevant to the security code requirements of the card brand.
* For example, on a Visa card there will be a <code>CVV</code> of 3 digits, whereas an
* American Express card requires a 4-digit <code>CID</code>.
* @property {string} code.name <code>"CVV"</code> <code>"CID"</code> <code>"CVC"</code>
* @property {number} code.size The expected length of the security code. Typically, this is 3 or 4.
*/
/**
* @name HostedFields#on
* @function
* @param {string} event The name of the event to which you are subscribing.
* @param {function} handler A callback to handle the event.
* @description Subscribes a handler function to a named event. `event` should be {@link HostedFields#event:blur|blur}, {@link HostedFields#event:focus|focus}, {@link HostedFields#event:empty|empty}, {@link HostedFields#event:notEmpty|notEmpty}, {@link HostedFields#event:cardTypeChange|cardTypeChange}, {@link HostedFields#event:validityChange|validityChange}, or {@link HostedFields#event:inputSubmitRequest|inputSubmitRequest}. Events will emit a {@link HostedFields~stateObject|stateObject}.
* @example
* <caption>Listening to a Hosted Field event, in this case 'focus'</caption>
* hostedFields.create({ ... }, function (createErr, hostedFieldsInstance) {
* hostedFieldsInstance.on('focus', function (event) {
* console.log(event.emittedBy, 'has been focused');
* });
* });
* @returns {void}
*/
/**
* This event is emitted when the user requests submission of an input field, such as by pressing the Enter or Return key on their keyboard, or mobile equivalent.
* @event HostedFields#inputSubmitRequest
* @type {HostedFields~stateObject}
* @example
* <caption>Clicking a submit button upon hitting Enter (or equivalent) within a Hosted Field</caption>
* var hostedFields = require('braintree-web/hosted-fields');
* var submitButton = document.querySelector('input[type="submit"]');
*
* hostedFields.create({ ... }, function (createErr, hostedFieldsInstance) {
* hostedFieldsInstance.on('inputSubmitRequest', function () {
* // User requested submission, e.g. by pressing Enter or equivalent
* submitButton.click();
* });
* });
*/
/**
* This event is emitted when a field transitions from having data to being empty.
* @event HostedFields#empty
* @type {HostedFields~stateObject}
* @example
* <caption>Listening to an empty event</caption>
* hostedFields.create({ ... }, function (createErr, hostedFieldsInstance) {
* hostedFieldsInstance.on('empty', function (event) {
* console.log(event.emittedBy, 'is now empty');
* });
* });
*/
/**
* This event is emitted when a field transitions from being empty to having data.
* @event HostedFields#notEmpty
* @type {HostedFields~stateObject}
* @example
* <caption>Listening to an notEmpty event</caption>
* hostedFields.create({ ... }, function (createErr, hostedFieldsInstance) {
* hostedFieldsInstance.on('notEmpty', function (event) {
* console.log(event.emittedBy, 'is now not empty');
* });
* });
*/
/**
* This event is emitted when a field loses focus.
* @event HostedFields#blur
* @type {HostedFields~stateObject}
* @example
* <caption>Listening to a blur event</caption>
* hostedFields.create({ ... }, function (createErr, hostedFieldsInstance) {
* hostedFieldsInstance.on('blur', function (event) {
* console.log(event.emittedBy, 'lost focus');
* });
* });
*/
/**
* This event is emitted when a field gains focus.
* @event HostedFields#focus
* @type {HostedFields~stateObject}
* @example
* <caption>Listening to a focus event</caption>
* hostedFields.create({ ... }, function (createErr, hostedFieldsInstance) {
* hostedFieldsInstance.on('focus', function (event) {
* console.log(event.emittedBy, 'gained focus');
* });
* });
*/
/**
* This event is emitted when activity within the number field has changed such that the possible card type has changed.
* @event HostedFields#cardTypeChange
* @type {HostedFields~stateObject}
* @example
* <caption>Listening to a cardTypeChange event</caption>
* hostedFields.create({ ... }, function (createErr, hostedFieldsInstance) {
* hostedFieldsInstance.on('cardTypeChange', function (event) {
* if (event.cards.length === 1) {
* console.log(event.cards[0].type);
* } else {
* console.log('Type of card not yet known');
* }
* });
* });
*/
/**
* This event is emitted when the validity of a field has changed. Validity is represented in the {@link HostedFields~stateObject|stateObject} as two booleans: `isValid` and `isPotentiallyValid`.
* @event HostedFields#validityChange
* @type {HostedFields~stateObject}
* @example
* <caption>Listening to a validityChange event</caption>
* hostedFields.create({ ... }, function (createErr, hostedFieldsInstance) {
* hostedFieldsInstance.on('validityChange', function (event) {
* var field = event.fields[event.emittedBy];
*
* if (field.isValid) {
* console.log(event.emittedBy, 'is fully valid');
* } else if (field.isPotentiallyValid) {
* console.log(event.emittedBy, 'is potentially valid');
* } else {
* console.log(event.emittedBy, 'is not valid');
* }
* });
* });
*/
function createInputEventHandler(fields) {
return function (eventData) {
var field;
var merchantPayload = eventData.merchantPayload;
var emittedBy = merchantPayload.emittedBy;
var container = fields[emittedBy].containerElement;
Object.keys(merchantPayload.fields).forEach(function (key) {
merchantPayload.fields[key].container = fields[key].containerElement;
});
field = merchantPayload.fields[emittedBy];
if (eventData.type === 'blur') {
performBlurFixForIos(container);
}
classList.toggle(container, constants.externalClasses.FOCUSED, field.isFocused);
classList.toggle(container, constants.externalClasses.VALID, field.isValid);
classList.toggle(container, constants.externalClasses.INVALID, !field.isPotentiallyValid);
this._state = { // eslint-disable-line no-invalid-this
cards: merchantPayload.cards,
fields: merchantPayload.fields
};
this._emit(eventData.type, merchantPayload); // eslint-disable-line no-invalid-this
};
}
// iOS Safari has a bug where inputs in iframes
// will not dismiss the keyboard when they lose
// focus. We create a hidden button input that we
// can focus on and blur to force the keyboard to
// dismiss. See #229
function performBlurFixForIos(container) {
var hiddenInput;
if (!browserDetection.isIos()) {
return;
}
if (document.activeElement === document.body) {
hiddenInput = container.querySelector('input');
if (!hiddenInput) {
hiddenInput = document.createElement('input');
hiddenInput.type = 'button';
hiddenInput.style.height = '0px';
hiddenInput.style.width = '0px';
hiddenInput.style.opacity = '0';
hiddenInput.style.padding = '0';
hiddenInput.style.position = 'absolute';
hiddenInput.style.left = '-200%';
hiddenInput.style.top = '0px';
container.insertBefore(hiddenInput, container.firstChild);
}
hiddenInput.focus();
hiddenInput.blur();
}
}
function isVisibleEnough(node) {
var boundingBox = node.getBoundingClientRect();
var verticalMidpoint = Math.floor(boundingBox.height / 2);
var horizontalMidpoint = Math.floor(boundingBox.width / 2);
return (
boundingBox.top < (global.innerHeight - verticalMidpoint || document.documentElement.clientHeight - verticalMidpoint) &&
boundingBox.right > horizontalMidpoint &&
boundingBox.bottom > verticalMidpoint &&
boundingBox.left < (global.innerWidth - horizontalMidpoint || document.documentElement.clientWidth - horizontalMidpoint)
);
}
function fieldsDOMOrder(configFields) {
var fieldPosition = [];
var fields = configFields;
var sortedFieldNames = [];
var validFieldsInUse = Object.keys(fields).filter(function (key) {
return allowedFields.hasOwnProperty(key);
});
validFieldsInUse.forEach(function (key) {
fieldPosition.push([key, fields[key].container]);
});
fieldPosition.sort(function (a, b) {
var element1 = a[1];
var element2 = b[1];
var position = element1.compareDocumentPosition(element2);
if (element1 === element2) { return 0; }
if (position & Node.DOCUMENT_POSITION_FOLLOWING || position & Node.DOCUMENT_POSITION_CONTAINED_BY) {
return -1;
} else if (position & Node.DOCUMENT_POSITION_PRECEDING || position & Node.DOCUMENT_POSITION_CONTAINS) {
return 1;
}
return 0;
});
fieldPosition.forEach(function (value) {
sortedFieldNames.push(value[0]);
});
return sortedFieldNames;
}
/**
* @class HostedFields
* @param {object} options The Hosted Fields {@link module:braintree-web/hosted-fields.create create} options.
* @description <strong>Do not use this constructor directly. Use {@link module:braintree-web/hosted-fields.create|braintree-web.hosted-fields.create} instead.</strong>
* @classdesc This class represents a Hosted Fields component produced by {@link module:braintree-web/hosted-fields.create|braintree-web/hosted-fields.create}. Instances of this class have methods for interacting with the input fields within Hosted Fields' iframes.
*/
function HostedFields(options) {
var failureTimeout, clientConfig, assetsUrl, isDebug, hostedFieldsUrl;
var self = this;
var fields = {};
var busOptions = assign({}, options);
var frameReadyPromiseResolveFunctions = {};
var frameReadyPromises = [];
var componentId = uuid();
if (options.client) {
clientConfig = options.client.getConfiguration();
assetsUrl = clientConfig.gatewayConfiguration.assetsUrl;
isDebug = clientConfig.isDebug;
} else {
assetsUrl = createAssetsUrl.create(options.authorization);
isDebug = Boolean(options.isDebug);
}
this._clientPromise = createDeferredClient.create({
client: options.client,
authorization: options.authorization,
debug: isDebug,
assetsUrl: assetsUrl,
name: 'Hosted Fields'
});
hostedFieldsUrl = composeUrl(assetsUrl, componentId, isDebug);
if (!options.fields || Object.keys(options.fields).length === 0) {
throw new BraintreeError({
type: sharedErrors.INSTANTIATION_OPTION_REQUIRED.type,
code: sharedErrors.INSTANTIATION_OPTION_REQUIRED.code,
message: 'options.fields is required when instantiating Hosted Fields.'
});
}
EventEmitter.call(this);
this._injectedNodes = [];
this._destructor = new Destructor();
this._fields = fields;
this._state = {
fields: {},
cards: getCardTypes('')
};
this._bus = new Bus({
channel: componentId,
merchantUrl: location.href
});
this._destructor.registerFunctionForTeardown(function () {
self._bus.teardown();
});
// NEXT_MAJOR_VERSION analytics events should have present tense verbs
if (!options.client) {
analytics.sendEvent(this._clientPromise, 'custom.hosted-fields.initialized.deferred-client');
} else {
analytics.sendEvent(this._clientPromise, 'custom.hosted-fields.initialized');
}
Object.keys(options.fields).forEach(function (key) {
var field, container, frame, frameReadyPromise;
if (!constants.allowedFields.hasOwnProperty(key)) {
throw new BraintreeError({
type: errors.HOSTED_FIELDS_INVALID_FIELD_KEY.type,
code: errors.HOSTED_FIELDS_INVALID_FIELD_KEY.code,
message: '"' + key + '" is not a valid field.'
});
}
field = options.fields[key];
container = document.querySelector(field.selector);
if (!container) {
throw new BraintreeError({
type: errors.HOSTED_FIELDS_INVALID_FIELD_SELECTOR.type,
code: errors.HOSTED_FIELDS_INVALID_FIELD_SELECTOR.code,
message: errors.HOSTED_FIELDS_INVALID_FIELD_SELECTOR.message,
details: {
fieldSelector: field.selector,
fieldKey: key
}
});
} else if (container.querySelector('iframe[name^="braintree-"]')) {
throw new BraintreeError({
type: errors.HOSTED_FIELDS_FIELD_DUPLICATE_IFRAME.type,
code: errors.HOSTED_FIELDS_FIELD_DUPLICATE_IFRAME.code,
message: errors.HOSTED_FIELDS_FIELD_DUPLICATE_IFRAME.message,
details: {
fieldSelector: field.selector,
fieldKey: key
}
});
}
if (field.maxlength && typeof field.maxlength !== 'number') {
throw new BraintreeError({
type: errors.HOSTED_FIELDS_FIELD_PROPERTY_INVALID.type,
code: errors.HOSTED_FIELDS_FIELD_PROPERTY_INVALID.code,
message: 'The value for maxlength must be a number.',
details: {
fieldKey: key
}
});
}
if (field.minlength && typeof field.minlength !== 'number') {
throw new BraintreeError({
type: errors.HOSTED_FIELDS_FIELD_PROPERTY_INVALID.type,
code: errors.HOSTED_FIELDS_FIELD_PROPERTY_INVALID.code,
message: 'The value for minlength must be a number.',
details: {
fieldKey: key
}
});
}
frame = iFramer({
type: key,
name: 'braintree-hosted-field-' + key,
style: constants.defaultIFrameStyle,
title: 'Secure Credit Card Frame - ' + constants.allowedFields[key].label
});
this._injectedNodes.push.apply(this._injectedNodes, injectFrame(frame, container));
this._setupLabelFocus(key, container);
fields[key] = {
frameElement: frame,
containerElement: container
};
frameReadyPromise = new Promise(function (resolve) {
frameReadyPromiseResolveFunctions[key] = resolve;
});
frameReadyPromises.push(frameReadyPromise);
this._state.fields[key] = {
isEmpty: true,
isValid: false,
isPotentiallyValid: true,
isFocused: false,
container: container
};
setTimeout(function () {
// Edge has an intermittent issue where
// the iframes load, but the JavaScript
// can't message out to the parent page.
// We can fix this by setting the src
// to about:blank first followed by
// the actual source. Both instances
// of setting the src need to be in a
// setTimeout to work.
if (browserDetection.isIE() || browserDetection.isEdge()) {
frame.src = 'about:blank';
setTimeout(function () {
frame.src = hostedFieldsUrl;
}, 0);
} else {
frame.src = hostedFieldsUrl;
}
}, 0);
}.bind(this));
busOptions.orderedFields = fieldsDOMOrder(this._state.fields);
if (busOptions.styles) {
Object.keys(busOptions.styles).forEach(function (selector) {
var className = busOptions.styles[selector];
if (typeof className === 'string') {
busOptions.styles[selector] = getStylesFromClass(className);
}
});
}
this._bus.on(events.READY_FOR_CLIENT, function (reply) {
self._clientPromise.then(function (client) {
reply(client);
});
});
this._bus.on(events.CARD_FORM_ENTRY_HAS_BEGUN, function () {
analytics.sendEvent(self._clientPromise, 'hosted-fields.input.started');
});
failureTimeout = setTimeout(function () {
analytics.sendEvent(self._clientPromise, 'custom.hosted-fields.load.timed-out');
self._emit('timeout');
}, INTEGRATION_TIMEOUT_MS);
Promise.all(frameReadyPromises).then(function (results) {
var reply = results[0];
clearTimeout(failureTimeout);
reply(busOptions);
self._emit('ready');
});
this._bus.on(events.FRAME_READY, function (data, reply) {
frameReadyPromiseResolveFunctions[data.field](reply);
});
this._bus.on(
events.INPUT_EVENT,
createInputEventHandler(fields).bind(this)
);
this._bus.on(events.TRIGGER_INPUT_FOCUS, function (fieldName) {
var container = fields[fieldName].containerElement;
// Inputs outside of the viewport don't always scroll into view on
// focus in iOS Safari. 5ms timeout gives the browser a chance to
// do the right thing and prevents stuttering.
if (browserDetection.isIos()) {
setTimeout(function () {
if (!isVisibleEnough(container)) {
container.scrollIntoView();
}
}, 5);
}
});
this._destructor.registerFunctionForTeardown(function () {
var j, node, parent;
for (j = 0; j < self._injectedNodes.length; j++) {
node = self._injectedNodes[j];
parent = node.parentNode;
parent.removeChild(node);
classList.remove(
parent,
constants.externalClasses.FOCUSED,
constants.externalClasses.INVALID,
constants.externalClasses.VALID
);
}
});
this._destructor.registerFunctionForTeardown(function () {
var methodNames = methods(HostedFields.prototype).concat(methods(EventEmitter.prototype));
convertMethodsToError(self, methodNames);
});
}
HostedFields.prototype = Object.create(EventEmitter.prototype, {
constructor: HostedFields
});
HostedFields.prototype._setupLabelFocus = function (type, container) {
var labels, i;
var shouldSkipLabelFocus = browserDetection.isIos();
var bus = this._bus;
if (shouldSkipLabelFocus) { return; }
if (container.id == null) { return; }
function triggerFocus() {
bus.emit(events.TRIGGER_INPUT_FOCUS, type);
}
labels = Array.prototype.slice.call(document.querySelectorAll('label[for="' + container.id + '"]'));
labels = labels.concat(findParentTags(container, 'label'));
for (i = 0; i < labels.length; i++) {
labels[i].addEventListener('click', triggerFocus, false);
}
this._destructor.registerFunctionForTeardown(function () {
for (i = 0; i < labels.length; i++) {
labels[i].removeEventListener('click', triggerFocus, false);
}
});
};
HostedFields.prototype._attachInvalidFieldContainersToError = function (err) {
if (!(err.details && err.details.invalidFieldKeys && err.details.invalidFieldKeys.length > 0)) {
return;
}
err.details.invalidFields = {};
err.details.invalidFieldKeys.forEach(function (field) {
err.details.invalidFields[field] = this._fields[field].containerElement;
}.bind(this));
};
/**
* Cleanly remove anything set up by {@link module:braintree-web/hosted-fields.create|create}.
* @public
* @param {callback} [callback] Called on completion, containing an error if one occurred. No data is returned if teardown completes successfully. If no callback is provided, `teardown` returns a promise.
* @example
* hostedFieldsInstance.teardown(function (teardownErr) {
* if (teardownErr) {
* console.error('Could not tear down Hosted Fields!');
* } else {
* console.info('Hosted Fields has been torn down!');
* }
* });
* @returns {Promise|void} Returns a promise if no callback is provided.
*/
HostedFields.prototype.teardown = function () {
var self = this;
return new Promise(function (resolve, reject) {
self._destructor.teardown(function (err) {
analytics.sendEvent(self._clientPromise, 'custom.hosted-fields.teardown-completed');
if (err) {
reject(err);
} else {
resolve();
}
});
});
};
/**
* Tokenizes fields and returns a nonce payload.
* @public
* @param {object} [options] All tokenization options for the Hosted Fields component.
* @param {boolean} [options.vault=false] When true, will vault the tokenized card. Cards will only be vaulted when using a client created with a client token that includes a customer ID.
* @param {array} [options.fieldsToTokenize] By default, all fields will be tokenized. You may specify which fields specifically you wish to tokenize with this property. Valid options are `'number'`, `'cvv'`, `'expirationDate'`, `'expirationMonth'`, `'expirationYear'`, `'postalCode'`.
* @param {string} [options.cardholderName] When supplied, the cardholder name to be tokenized with the contents of the fields.
* @param {string} [options.billingAddress.postalCode] When supplied, this postal code will be tokenized along with the contents of the fields. If a postal code is provided as part of the Hosted Fields configuration, the value of the field will be tokenized and this value will be ignored.
* @param {string} [options.billingAddress.firstName] When supplied, this customer first name will be tokenized along with the contents of the fields.
* @param {string} [options.billingAddress.lastName] When supplied, this customer last name will be tokenized along with the contents of the fields.
* @param {string} [options.billingAddress.company] When supplied, this company name will be tokenized along with the contents of the fields.
* @param {string} [options.billingAddress.streetAddress] When supplied, this street address will be tokenized along with the contents of the fields.
* @param {string} [options.billingAddress.extendedAddress] When supplied, this extended address will be tokenized along with the contents of the fields.
* @param {string} [options.billingAddress.locality] When supplied, this locality (the city) will be tokenized along with the contents of the fields.
* @param {string} [options.billingAddress.region] When supplied, this region (the state) will be tokenized along with the contents of the fields.
* @param {string} [options.billingAddress.countryCodeNumeric] When supplied, this numeric country code will be tokenized along with the contents of the fields.
* @param {string} [options.billingAddress.countryCodeAlpha2] When supplied, this alpha 2 representation of a country will be tokenized along with the contents of the fields.
* @param {string} [options.billingAddress.countryCodeAlpha3] When supplied, this alpha 3 representation of a country will be tokenized along with the contents of the fields.
* @param {string} [options.billingAddress.countryName] When supplied, this country name will be tokenized along with the contents of the fields.
*
* @param {callback} [callback] The second argument, <code>data</code>, is a {@link HostedFields~tokenizePayload|tokenizePayload}. If no callback is provided, `tokenize` returns a function that resolves with a {@link HostedFields~tokenizePayload|tokenizePayload}.
* @example <caption>Tokenize a card</caption>
* hostedFieldsInstance.tokenize(function (tokenizeErr, payload) {
* if (tokenizeErr) {
* switch (tokenizeErr.code) {
* case 'HOSTED_FIELDS_FIELDS_EMPTY':
* // occurs when none of the fields are filled in
* console.error('All fields are empty! Please fill out the form.');
* break;
* case 'HOSTED_FIELDS_FIELDS_INVALID':
* // occurs when certain fields do not pass client side validation
* console.error('Some fields are invalid:', tokenizeErr.details.invalidFieldKeys);
*
* // you can also programtically access the field containers for the invalid fields
* tokenizeErr.details.invalidFields.forEach(function (fieldContainer) {
* fieldContainer.className = 'invalid';
* });
* break;
* case 'HOSTED_FIELDS_TOKENIZATION_FAIL_ON_DUPLICATE':
* // occurs when:
* // * the client token used for client authorization was generated
* // with a customer ID and the fail on duplicate payment method
* // option is set to true
* // * the card being tokenized has previously been vaulted (with any customer)
* // See: https://developers.braintreepayments.com/reference/request/client-token/generate/#options.fail_on_duplicate_payment_method
* console.error('This payment method already exists in your vault.');
* break;
* case 'HOSTED_FIELDS_TOKENIZATION_CVV_VERIFICATION_FAILED':
* // occurs when:
* // * the client token used for client authorization was generated
* // with a customer ID and the verify card option is set to true
* // and you have credit card verification turned on in the Braintree
* // control panel
* // * the cvv does not pass verfication (https://developers.braintreepayments.com/reference/general/testing/#avs-and-cvv/cid-responses)
* // See: https://developers.braintreepayments.com/reference/request/client-token/generate/#options.verify_card
* console.error('CVV did not pass verification');
* break;
* case 'HOSTED_FIELDS_FAILED_TOKENIZATION':
* // occurs for any other tokenization error on the server
* console.error('Tokenization failed server side. Is the card valid?');
* break;
* case 'HOSTED_FIELDS_TOKENIZATION_NETWORK_ERROR':
* // occurs when the Braintree gateway cannot be contacted
* console.error('Network error occurred when tokenizing.');
* break;
* default:
* console.error('Something bad happened!', tokenizeErr);
* }
* } else {
* console.log('Got nonce:', payload.nonce);
* }
* });
* @example <caption>Tokenize and vault a card</caption>
* hostedFieldsInstance.tokenize({
* vault: true
* }, function (tokenizeErr, payload) {
* if (tokenizeErr) {
* console.error(tokenizeErr);
* } else {
* console.log('Got nonce:', payload.nonce);
* }
* });
* @example <caption>Tokenize a card with cardholder name</caption>
* hostedFieldsInstance.tokenize({
* cardholderName: 'First Last'
* }, function (tokenizeErr, payload) {
* if (tokenizeErr) {
* console.error(tokenizeErr);
* } else {
* console.log('Got nonce:', payload.nonce);
* }
* });
* @example <caption>Tokenize a card with the postal code option</caption>
* hostedFieldsInstance.tokenize({
* billingAddress: {
* postalCode: '11111'
* }
* }, function (tokenizeErr, payload) {
* if (tokenizeErr) {
* console.error(tokenizeErr);
* } else {
* console.log('Got nonce:', payload.nonce);
* }
* });
* @example <caption>Tokenize a card with additional billing address options</caption>
* hostedFieldsInstance.tokenize({
* billingAddress: {
* firstName: 'First',
* lastName: 'Last',
* company: 'Company',
* streetAddress: '123 Street',
* extendedAddress: 'Unit 1',
* // passing just one of the country options is sufficient to
* // associate the card details with a particular country
* // valid country names and codes can be found here:
* // https://developers.braintreepayments.com/reference/general/countries/ruby#list-of-countries
* countryName: 'United States',
* countryCodeAlpha2: 'US',
* countryCodeAlpha3: 'USA',
* countryCodeNumeric: '840'
* }
* }, function (tokenizeErr, payload) {
* if (tokenizeErr) {
* console.error(tokenizeErr);
* } else {
* console.log('Got nonce:', payload.nonce);
* }
* });
* @returns {Promise|void} Returns a promise if no callback is provided.
*/
HostedFields.prototype.tokenize = function (options) {
var self = this;
if (!options) {
options = {};
}
return new Promise(function (resolve, reject) {
self._bus.emit(events.TOKENIZATION_REQUEST, options, function (response) {
var err = response[0];
var payload = response[1];
if (err) {
self._attachInvalidFieldContainersToError(err);
reject(new BraintreeError(err));
} else {
resolve(payload);
}
});
});
};
/**
* Add a class to a {@link module:braintree-web/hosted-fields~field field}. Useful for updating field styles when events occur elsewhere in your checkout.
* @public
* @param {string} field The field you wish to add a class to. Must be a valid {@link module:braintree-web/hosted-fields~fieldOptions fieldOption}.
* @param {string} classname The class to be added.
* @param {callback} [callback] Callback executed on completion, containing an error if one occurred. No data is returned if the class is added successfully.
*
* @example
* hostedFieldsInstance.addClass('number', 'custom-class', function (addClassErr) {
* if (addClassErr) {
* console.error(addClassErr);
* }
* });
* @returns {Promise|void} Returns a promise if no callback is provided.
*/
HostedFields.prototype.addClass = function (field, classname) {
var err;
if (!allowedFields.hasOwnProperty(field)) {
err = new BraintreeError({
type: errors.HOSTED_FIELDS_FIELD_INVALID.type,
code: errors.HOSTED_FIELDS_FIELD_INVALID.code,
message: '"' + field + '" is not a valid field. You must use a valid field option when adding a class.'
});
} else if (!this._fields.hasOwnProperty(field)) {
err = new BraintreeError({
type: errors.HOSTED_FIELDS_FIELD_NOT_PRESENT.type,
code: errors.HOSTED_FIELDS_FIELD_NOT_PRESENT.code,
message: 'Cannot add class to "' + field + '" field because it is not part of the current Hosted Fields options.'
});
} else {
this._bus.emit(events.ADD_CLASS, field, classname);
}
if (err) {
return Promise.reject(err);
}
return Promise.resolve();
};
/**
* Removes a class to a {@link module:braintree-web/hosted-fields~field field}. Useful for updating field styles when events occur elsewhere in your checkout.
* @public
* @param {string} field The field you wish to remove a class from. Must be a valid {@link module:braintree-web/hosted-fields~fieldOptions fieldOption}.
* @param {string} classname The class to be removed.
* @param {callback} [callback] Callback executed on completion, containing an error if one occurred. No data is returned if the class is removed successfully.
*
* @example
* hostedFieldsInstance.addClass('number', 'custom-class', function (addClassErr) {
* if (addClassErr) {
* console.error(addClassErr);
* return;
* }
*
* // some time later...
* hostedFieldsInstance.removeClass('number', 'custom-class');
* });
* @returns {Promise|void} Returns a promise if no callback is provided.
*/
HostedFields.prototype.removeClass = function (field, classname) {
var err;
if (!allowedFields.hasOwnProperty(field)) {
err = new BraintreeError({
type: errors.HOSTED_FIELDS_FIELD_INVALID.type,
code: errors.HOSTED_FIELDS_FIELD_INVALID.code,
message: '"' + field + '" is not a valid field. You must use a valid field option when removing a class.'
});
} else if (!this._fields.hasOwnProperty(field)) {
err = new BraintreeError({
type: errors.HOSTED_FIELDS_FIELD_NOT_PRESENT.type,
code: errors.HOSTED_FIELDS_FIELD_NOT_PRESENT.code,
message: 'Cannot remove class from "' + field + '" field because it is not part of the current Hosted Fields options.'
});
} else {
this._bus.emit(events.REMOVE_CLASS, field, classname);
}
if (err) {
return Promise.reject(err);
}
return Promise.resolve();
};
/**
* Sets an attribute of a {@link module:braintree-web/hosted-fields~field field}.
* Supported attributes are `aria-invalid`, `aria-required`, `disabled`, and `placeholder`.
*
* @public
* @param {object} options The options for the attribute you wish to set.
* @param {string} options.field The field to which you wish to add an attribute. Must be a valid {@link module:braintree-web/hosted-fields~fieldOptions fieldOption}.
* @param {string} options.attribute The name of the attribute you wish to add to the field.
* @param {string} options.value The value for the attribute.
* @param {callback} [callback] Callback executed on completion, containing an error if one occurred. No data is returned if the attribute is set successfully.
*
* @example <caption>Set the placeholder attribute of a field</caption>
* hostedFieldsInstance.setAttribute({
* field: 'number',
* attribute: 'placeholder',
* value: '1111 1111 1111 1111'
* }, function (attributeErr) {
* if (attributeErr) {
* console.error(attributeErr);
* }
* });
*
* @example <caption>Set the aria-required attribute of a field</caption>
* hostedFieldsInstance.setAttribute({
* field: 'number',
* attribute: 'aria-required',
* value: true
* }, function (attributeErr) {
* if (attributeErr) {
* console.error(attributeErr);
* }
* });
*
* @returns {Promise|void} Returns a promise if no callback is provided.
*/
HostedFields.prototype.setAttribute = function (options) {
var attributeErr, err;
if (!allowedFields.hasOwnProperty(options.field)) {
err = new BraintreeError({
type: errors.HOSTED_FIELDS_FIELD_INVALID.type,
code: errors.HOSTED_FIELDS_FIELD_INVALID.code,
message: '"' + options.field + '" is not a valid field. You must use a valid field option when setting an attribute.'
});
} else if (!this._fields.hasOwnProperty(options.field)) {
err = new BraintreeError({
type: errors.HOSTED_FIELDS_FIELD_NOT_PRESENT.type,
code: errors.HOSTED_FIELDS_FIELD_NOT_PRESENT.code,
message: 'Cannot set attribute for "' + options.field + '" field because it is not part of the current Hosted Fields options.'
});
} else {
attributeErr = attributeValidationError(options.attribute, options.value);
if (attributeErr) {
err = attributeErr;
} else {
this._bus.emit(events.SET_ATTRIBUTE, options.field, options.attribute, options.value);
}
}
if (err) {
return Promise.reject(err);
}
return Promise.resolve();
};
/**
* Sets a visually hidden message (for screenreaders) on a {@link module:braintree-web/hosted-fields~field field}.
*
* @public
* @param {object} options The options for the attribute you wish to set.
* @param {string} options.field The field to which you wish to add an attribute. Must be a valid {@link module:braintree-web/hosted-fields~field field}.
* @param {string} options.message The message to set.
*
* @example <caption>Set an error message on a field</caption>
* hostedFieldsInstance.setMessage({
* field: 'number',
* message: 'Invalid card number'
* });
*
* @example <caption>Remove the message on a field</caption>
* hostedFieldsInstance.setMessage({
* field: 'number',
* message: ''
* });
*
* @returns {void}
*/
HostedFields.prototype.setMessage = function (options) {
this._bus.emit(events.SET_MESSAGE, options.field, options.message);
};
/**
* Removes a supported attribute from a {@link module:braintree-web/hosted-fields~field field}.
*
* @public
* @param {object} options The options for the attribute you wish to remove.
* @param {string} options.field The field from which you wish to remove an attribute. Must be a valid {@link module:braintree-web/hosted-fields~fieldOptions fieldOption}.
* @param {string} options.attribute The name of the attribute you wish to remove from the field.
* @param {callback} [callback] Callback executed on completion, containing an error if one occurred. No data is returned if the attribute is removed successfully.
*
* @example <caption>Remove the placeholder attribute of a field</caption>
* hostedFieldsInstance.removeAttribute({
* field: 'number',
* attribute: 'placeholder'
* }, function (attributeErr) {
* if (attributeErr) {
* console.error(attributeErr);
* }
* });
*
* @returns {Promise|void} Returns a promise if no callback is provided.
*/
HostedFields.prototype.removeAttribute = function (options) {
var attributeErr, err;
if (!allowedFields.hasOwnProperty(options.field)) {
err = new BraintreeError({
type: errors.HOSTED_FIELDS_FIELD_INVALID.type,
code: errors.HOSTED_FIELDS_FIELD_INVALID.code,
message: '"' + options.field + '" is not a valid field. You must use a valid field option when removing an attribute.'
});
} else if (!this._fields.hasOwnProperty(options.field)) {
err = new BraintreeError({
type: errors.HOSTED_FIELDS_FIELD_NOT_PRESENT.type,
code: errors.HOSTED_FIELDS_FIELD_NOT_PRESENT.code,
message: 'Cannot remove attribute for "' + options.field + '" field because it is not part of the current Hosted Fields options.'
});
} else {
attributeErr = attributeValidationError(options.attribute);
if (attributeErr) {
err = attributeErr;
} else {
this._bus.emit(events.REMOVE_ATTRIBUTE, options.field, options.attribute);
}
}
if (err) {
return Promise.reject(err);
}
return Promise.resolve();
};
/**
* @deprecated since version 3.8.0. Use {@link HostedFields#setAttribute|setAttribute} instead.
*
* @public
* @param {string} field The field whose placeholder you wish to change. Must be a valid {@link module:braintree-web/hosted-fields~fieldOptions fieldOption}.
* @param {string} placeholder Will be used as the `placeholder` attribute of the input.
* @param {callback} [callback] Callback executed on completion, containing an error if one occurred. No data is returned if the placeholder updated successfully.
*
* @returns {Promise|void} Returns a promise if no callback is provided.
*/
HostedFields.prototype.setPlaceholder = function (field, placeholder) {
return this.setAttribute({
field: field,
attribute: 'placeholder',
value: placeholder
});
};
/**
* Clear the value of a {@link module:braintree-web/hosted-fields~field field}.
* @public
* @param {string} field The field you wish to clear. Must be a valid {@link module:braintree-web/hosted-fields~fieldOptions fieldOption}.
* @param {callback} [callback] Callback executed on completion, containing an error if one occurred. No data is returned if the field cleared successfully.
* @returns {Promise|void} Returns a promise if no callback is provided.
* @example
* hostedFieldsInstance.clear('number', function (clearErr) {
* if (clearErr) {
* console.error(clearErr);
* }
* });
*
* @example <caption>Clear several fields</caption>
* hostedFieldsInstance.clear('number');
* hostedFieldsInstance.clear('cvv');
* hostedFieldsInstance.clear('expirationDate');
*/
HostedFields.prototype.clear = function (field) {
var err;
if (!allowedFields.hasOwnProperty(field)) {
err = new BraintreeError({
type: errors.HOSTED_FIELDS_FIELD_INVALID.type,
code: errors.HOSTED_FIELDS_FIELD_INVALID.code,
message: '"' + field + '" is not a valid field. You must use a valid field option when clearing a field.'
});
} else if (!this._fields.hasOwnProperty(field)) {
err = new BraintreeError({
type: errors.HOSTED_FIELDS_FIELD_NOT_PRESENT.type,
code: errors.HOSTED_FIELDS_FIELD_NOT_PRESENT.code,
message: 'Cannot clear "' + field + '" field because it is not part of the current Hosted Fields options.'
});
} else {
this._bus.emit(events.CLEAR_FIELD, field);
}
if (err) {
return Promise.reject(err);
}
return Promise.resolve();
};
/**
* Programmatically focus a {@link module:braintree-web/hosted-fields~field field}.
* @public
* @param {string} field The field you want to focus. Must be a valid {@link module:braintree-web/hosted-fields~fieldOptions fieldOption}.
* @param {callback} [callback] Callback executed on completion, containing an error if one occurred. No data is returned if the field focused successfully.
* @returns {void}
* @example
* hostedFieldsInstance.focus('number', function (focusErr) {
* if (focusErr) {
* console.error(focusErr);
* }
* });
* @example <caption>Using an event listener</caption>
* myElement.addEventListener('click', function (e) {
* // In Firefox, the focus method can be suppressed
* // if the element has a tabindex property or the element
* // is an anchor link with an href property.
* // In Mobile Safari, the focus method is unable to
* // programatically open the keyboard, as only
* // touch events are allowed to do so.
* e.preventDefault();
* hostedFieldsInstance.focus('number');
* });
*/
HostedFields.prototype.focus = function (field) {
var err;
if (!allowedFields.hasOwnProperty(field)) {
err = new BraintreeError({
type: errors.HOSTED_FIELDS_FIELD_INVALID.type,
code: errors.HOSTED_FIELDS_FIELD_INVALID.code,
message: '"' + field + '" is not a valid field. You must use a valid field option when focusing a field.'
});
} else if (!this._fields.hasOwnProperty(field)) {
err = new BraintreeError({
type: errors.HOSTED_FIELDS_FIELD_NOT_PRESENT.type,
code: errors.HOSTED_FIELDS_FIELD_NOT_PRESENT.code,
message: 'Cannot focus "' + field + '" field because it is not part of the current Hosted Fields options.'
});
} else {
this._bus.emit(events.TRIGGER_INPUT_FOCUS, field);
}
if (err) {
return Promise.reject(err);
}
return Promise.resolve();
};
/**
* Returns an {@link HostedFields~stateObject|object} that includes the state of all fields and possible card types.
* @public
* @returns {object} {@link HostedFields~stateObject|stateObject}
* @example <caption>Check if all fields are valid</caption>
* var state = hostedFieldsInstance.getState();
*
* var formValid = Object.keys(state.fields).every(function (key) {
* return state.fields[key].isValid;
* });
*/
HostedFields.prototype.getState = function () {
return this._state;
};
module.exports = wrapPromise.wrapPrototype(HostedFields);