Feature/preconfiguring aria config objects (#221)

* Ensure ariaConfig object always has an iframeTitle property

* Initialiser of SecuredField takes responsibility for ensuring a default ariaConfig object

* SecuredField takes responsibility for adding translated error property

* ariaConfig object processing moved to own file

* Renaming ariaLabels prop to ariaConfig

* Adding types

* Initialiser of securedField takes responsibility for adding translated placeholder, if required.
Ensures fields don't get values in their placeholder config that don't apply to their group

* When ariaConfig object already exists - destructure it to remove existing references from memory

* changed folder name

* syncing processAriaConfig

* Pass i18n prop from SecuredFieldsProvider into CSF so it can reach SecuredField.ts (since useCoreContext is not available anymore)

* Removed unnecessary interface. Added comments.

* Removed logs

* Small refactor, responding to comment

* Added tests. Restored default values to commonConfig.js
This commit is contained in:
sponglord
2020-08-27 15:19:05 +02:00
committed by GitHub
parent 8f046f1e57
commit ded1d4e99b
10 changed files with 308 additions and 69 deletions

View File

@@ -27,7 +27,7 @@ export const ariaLabels = {
encryptedCardNumber: {
label: 'Credit or debit card number field',
iframeTitle: 'Iframe for credit card number field'
// error: "credit card number is in error"
// error: 'credit card number is in error'
},
encryptedExpiryDate: {
label: 'Credit or debit card expiration date field'

View File

@@ -1,14 +1,5 @@
import { Component } from 'preact';
import {
getErrorObject,
getFields,
getErrorReducer,
validFieldsReducer,
addTranslationsToObject,
getTranslatedErrors,
resolvePlaceholders
} from './utils';
import { CSF_FIELDS_ARRAY } from './lib/configuration/constants';
import { getErrorObject, getFields, getErrorReducer, validFieldsReducer } from './utils';
import initCSF from './lib';
import handlers from './SecuredFieldsProviderHandlers';
import defaultProps, { SFPProps } from './defaultProps';
@@ -153,12 +144,10 @@ class SecuredFieldsProvider extends Component<SFPProps, SFPState> {
showWarnings: this.props.showWarnings,
iframeUIConfig: {
sfStyles: this.props.styles,
placeholders: {
...resolvePlaceholders(this.props.i18n),
...this.props.placeholders
},
ariaLabels: addTranslationsToObject(this.props.ariaLabels, CSF_FIELDS_ARRAY, 'error', getTranslatedErrors(this.props.i18n))
placeholders: this.props.placeholders,
ariaConfig: this.props.ariaLabels
},
i18n: this.props.i18n,
callbacks: {
onLoad: this.handleOnLoad,
onConfigSuccess: this.handleOnConfigSuccess,

View File

@@ -28,33 +28,33 @@ export interface IframeConfigObject extends SFInternalConfig {
fieldType: string;
cvcRequired: boolean;
numKey: number;
pmConfig?: any; // TODO - only needed until latest version of 3.2.2 is on test
}
interface IframeUIConfigObject {
sfStyles?: StylesObject;
placeholders?: PlaceholdersObject;
ariaLabels?: AriaLabels;
ariaConfig?: AriaConfig;
ariaLabels?: AriaConfig; // TODO - only needed until latest version of SF 3.2.5 is on test
}
interface PlaceholdersObject {
export interface PlaceholdersObject {
[key: string]: string; // e.g. encryptedExpiryDate: 'MM/YY'
}
type AriaLabels = {
export type AriaConfig = {
lang?: string;
} & {
[key: string]: AriaLabelsObject; // e.g. encryptedCardNumber: {...}
[key: string]: AriaConfigObject; // e.g. encryptedCardNumber: {...}
};
interface AriaLabelsObject {
export interface AriaConfigObject {
iframeTitle?: string;
label?: string;
error?: string;
}
abstract class AbstractSecuredField {
protected config: SFInternalConfig;
public config: SFInternalConfig; // could be protected but needs to be public for tests to run
protected fieldType: string;
protected cvcRequired: boolean;
protected iframeSrc: string;

View File

@@ -0,0 +1,173 @@
import SecuredField from './SecuredField';
import { AriaConfig } from './AbstractSecuredField';
import Language from '../../../../../language/Language';
import { IFRAME_TITLE } from '../configuration/constants';
import LANG from '../../../../../language/locales/nl-NL.json';
const ENCRYPTED_CARD_NUMBER = 'encryptedCardNumber';
const ENCRYPTED_EXPIRY_DATE = 'encryptedExpiryDate';
const ENCRYPTED_SECURITY_CODE = 'encryptedSecurityCode';
const TRANSLATED_NUMBER_ERROR = LANG['creditCard.numberField.invalid'];
const TRANSLATED_DATE_ERROR = LANG['creditCard.expiryDateField.invalid'];
const TRANSLATED_CVC_ERROR = LANG['creditCard.oneClickVerification.invalidInput.title'];
const TRANSLATED_NUMBER_PLACEHOLDER = LANG['creditCard.numberField.placeholder'];
const TRANSLATED_DATE_PLACEHOLDER = LANG['creditCard.expiryDateField.placeholder'];
const TRANSLATED_CVC_PLACEHOLDER = LANG['creditCard.cvcField.placeholder'];
const nodeHolder = document.createElement('div');
const i18n = new Language('nl-NL', {});
const mockAriaConfig = {
lang: 'en-GB',
encryptedCardNumber: {
label: 'Credit or debit card number field',
iframeTitle: 'Iframe for credit card number field'
},
encryptedExpiryDate: {
label: 'Credit or debit card expiration date field',
error: 'Date field is in error'
},
encryptedSecurityCode: {
label: 'Credit or debit card 3 or 4 digit security code field'
}
} as AriaConfig;
const mockPlaceholders = {
encryptedCardNumber: '9999 9999 9999 9999',
encryptedExpiryDate: 'MM/YY',
encryptedSecurityCode: 999
};
const iframeUIConfig = {
placeholders: null,
sfStyles: null,
ariaConfig: {}
};
const setupObj = {
extraFieldData: null,
txVariant: 'card',
cardGroupTypes: ['amex', 'mc', 'visa'],
iframeUIConfig,
sfLogAtStart: false,
trimTrailingSeparator: false,
isCreditCardType: true,
showWarnings: false,
//
fieldType: ENCRYPTED_CARD_NUMBER,
cvcRequired: true,
iframeSrc: null,
loadingContext: null,
holderEl: nodeHolder
};
describe('SecuredField handling ariaConfig object - should set defaults', () => {
//
test('Card number field with no defined ariaConfig should get default title & translated error props', () => {
const card = new SecuredField(setupObj, i18n);
expect(card.config.iframeUIConfig.ariaConfig[ENCRYPTED_CARD_NUMBER].iframeTitle).toEqual(IFRAME_TITLE);
expect(card.config.iframeUIConfig.ariaConfig[ENCRYPTED_CARD_NUMBER].error).toEqual(TRANSLATED_NUMBER_ERROR);
});
test('Date field with no defined ariaConfig should get default title & translated error props', () => {
setupObj.fieldType = ENCRYPTED_EXPIRY_DATE;
const card = new SecuredField(setupObj, i18n);
expect(card.config.iframeUIConfig.ariaConfig[ENCRYPTED_EXPIRY_DATE].iframeTitle).toEqual(IFRAME_TITLE);
expect(card.config.iframeUIConfig.ariaConfig[ENCRYPTED_EXPIRY_DATE].error).toEqual(TRANSLATED_DATE_ERROR);
});
test('CVC field with no defined ariaConfig should get default title & translated error props', () => {
setupObj.fieldType = ENCRYPTED_SECURITY_CODE;
const card = new SecuredField(setupObj, i18n);
expect(card.config.iframeUIConfig.ariaConfig[ENCRYPTED_SECURITY_CODE].iframeTitle).toEqual(IFRAME_TITLE);
expect(card.config.iframeUIConfig.ariaConfig[ENCRYPTED_SECURITY_CODE].error).toEqual(TRANSLATED_CVC_ERROR);
});
});
describe('SecuredField handling ariaConfig object - should set defaults only where they are not already defined', () => {
//
test('Card number field with ariaConfig with label & iframeTitle should preserve these props and also get a translated error prop', () => {
setupObj.iframeUIConfig.ariaConfig = mockAriaConfig;
setupObj.fieldType = ENCRYPTED_CARD_NUMBER;
const card = new SecuredField(setupObj, i18n);
expect(card.config.iframeUIConfig.ariaConfig[ENCRYPTED_CARD_NUMBER].label).toEqual(mockAriaConfig[ENCRYPTED_CARD_NUMBER].label);
expect(card.config.iframeUIConfig.ariaConfig[ENCRYPTED_CARD_NUMBER].iframeTitle).toEqual(mockAriaConfig[ENCRYPTED_CARD_NUMBER].iframeTitle);
expect(card.config.iframeUIConfig.ariaConfig[ENCRYPTED_CARD_NUMBER].error).toEqual(TRANSLATED_NUMBER_ERROR);
// Also check that configured language is preserved
expect(card.config.iframeUIConfig.ariaConfig.lang).toEqual(mockAriaConfig.lang);
});
test('Date field with ariaConfig with label & error should preserve these props and also get a iframeTitle prop', () => {
setupObj.fieldType = ENCRYPTED_EXPIRY_DATE;
const card = new SecuredField(setupObj, i18n);
expect(card.config.iframeUIConfig.ariaConfig[ENCRYPTED_EXPIRY_DATE].label).toEqual(mockAriaConfig[ENCRYPTED_EXPIRY_DATE].label);
expect(card.config.iframeUIConfig.ariaConfig[ENCRYPTED_EXPIRY_DATE].iframeTitle).toEqual(IFRAME_TITLE);
expect(card.config.iframeUIConfig.ariaConfig[ENCRYPTED_EXPIRY_DATE].error).toEqual(mockAriaConfig[ENCRYPTED_EXPIRY_DATE].error);
});
test('CVC field with ariaConfig with label should preserve this prop and also get default title & translated error props', () => {
setupObj.fieldType = ENCRYPTED_SECURITY_CODE;
const card = new SecuredField(setupObj, i18n);
expect(card.config.iframeUIConfig.ariaConfig[ENCRYPTED_SECURITY_CODE].label).toEqual(mockAriaConfig[ENCRYPTED_SECURITY_CODE].label);
expect(card.config.iframeUIConfig.ariaConfig[ENCRYPTED_SECURITY_CODE].iframeTitle).toEqual(IFRAME_TITLE);
expect(card.config.iframeUIConfig.ariaConfig[ENCRYPTED_SECURITY_CODE].error).toEqual(TRANSLATED_CVC_ERROR);
});
});
describe('SecuredField handling placeholders config object - should set defaults', () => {
//
test('Card number field with no defined placeholders config should get default value from translation field', () => {
setupObj.fieldType = ENCRYPTED_CARD_NUMBER;
const card = new SecuredField(setupObj, i18n);
expect(card.config.iframeUIConfig.placeholders[ENCRYPTED_CARD_NUMBER]).toEqual(TRANSLATED_NUMBER_PLACEHOLDER);
});
test('Date field with no defined placeholders config should get default value from translation field', () => {
setupObj.fieldType = ENCRYPTED_EXPIRY_DATE;
const card = new SecuredField(setupObj, i18n);
expect(card.config.iframeUIConfig.placeholders[ENCRYPTED_EXPIRY_DATE]).toEqual(TRANSLATED_DATE_PLACEHOLDER);
});
test('CVC field with no defined placeholders config should get default value from translation field', () => {
setupObj.fieldType = ENCRYPTED_SECURITY_CODE;
const card = new SecuredField(setupObj, i18n);
expect(card.config.iframeUIConfig.placeholders[ENCRYPTED_SECURITY_CODE]).toEqual(TRANSLATED_CVC_PLACEHOLDER);
});
});
describe('SecuredField handling placeholders config object - defined placeholder should be preserved', () => {
//
test('Card number field with defined placeholders should keep that value', () => {
setupObj.iframeUIConfig.placeholders = mockPlaceholders;
setupObj.fieldType = ENCRYPTED_CARD_NUMBER;
const card = new SecuredField(setupObj, i18n);
expect(card.config.iframeUIConfig.placeholders[ENCRYPTED_CARD_NUMBER]).toEqual(mockPlaceholders[ENCRYPTED_CARD_NUMBER]);
});
test('Date field with defined placeholders should keep that value', () => {
setupObj.fieldType = ENCRYPTED_EXPIRY_DATE;
const card = new SecuredField(setupObj, i18n);
expect(card.config.iframeUIConfig.placeholders[ENCRYPTED_EXPIRY_DATE]).toEqual(mockPlaceholders[ENCRYPTED_EXPIRY_DATE]);
});
test('CVC field with defined placeholders should keep that value', () => {
setupObj.fieldType = ENCRYPTED_SECURITY_CODE;
const card = new SecuredField(setupObj, i18n);
expect(card.config.iframeUIConfig.placeholders[ENCRYPTED_SECURITY_CODE]).toEqual(mockPlaceholders[ENCRYPTED_SECURITY_CODE]);
});
});

View File

@@ -3,7 +3,7 @@ import createIframe from '../utilities/createIframe';
import { selectOne, on, off, removeAllChildren } from '../utilities/dom';
import postMessageToIframe from './utils/iframes/postMessageToIframe';
import { isWebpackPostMsg, originCheckPassed, isChromeVoxPostMsg } from './utils/iframes/postMessageValidation';
import { ENCRYPTED_SECURITY_CODE, IFRAME_TITLE } from '../configuration/constants';
import { ENCRYPTED_SECURITY_CODE } from '../configuration/constants';
import { generateRandomNumber } from '../utilities/commonUtils';
import { SFFeedbackObj } from '../types';
import AbstractSecuredField, {
@@ -11,17 +11,21 @@ import AbstractSecuredField, {
IframeConfigObject,
RtnType_noParamVoidFn,
RtnType_postMessageListener,
RtnType_callbackFn
RtnType_callbackFn,
AriaConfig,
PlaceholdersObject
} from '../core/AbstractSecuredField';
import { pick, reject } from '../../utils';
import getProp from '../../../../../utils/getProp';
import { processAriaConfig } from './utils/init/processAriaConfig';
import { processPlaceholders } from './utils/init/processPlaceholders';
import Language from '../../../../../language/Language';
const logPostMsg = false;
const doLog = false;
class SecuredField extends AbstractSecuredField {
// --
constructor(pSetupObj: SFSetupObject) {
constructor(pSetupObj: SFSetupObject, i18n: Language) {
super();
// List of props from setup object not needed for iframe config
@@ -53,13 +57,28 @@ class SecuredField extends AbstractSecuredField {
logger.log('\n');
}
return this.init();
return this.init(i18n);
}
init(): SecuredField {
const iframeTitle: string = getProp(this.config, `iframeUIConfig.ariaLabels.${this.fieldType}.iframeTitle`) || IFRAME_TITLE;
init(i18n: Language): SecuredField {
/**
* Ensure all fields have a related ariaConfig object containing, at minimum, an iframeTitle property and a (translated) error
*/
const processedAriaConfig: AriaConfig = processAriaConfig(this.config, this.fieldType, i18n);
// Set result back onto config object
this.config.iframeUIConfig.ariaConfig = processedAriaConfig;
const iframeEl: HTMLIFrameElement = createIframe(`${this.iframeSrc}`, iframeTitle);
/**
* Ensure that if a placeholder hasn't been set for a field then it gets a default, translated, one
*/
const processedPlaceholders: PlaceholdersObject = processPlaceholders(this.config, this.fieldType, i18n);
// Set result back onto config object
this.config.iframeUIConfig.placeholders = processedPlaceholders;
/**
* Create & reference iframe and add load listener
*/
const iframeEl: HTMLIFrameElement = createIframe(`${this.iframeSrc}`, processedAriaConfig[this.fieldType].iframeTitle);
// Place the iframe into the holder
this.holderEl.appendChild(iframeEl);
@@ -93,6 +112,9 @@ class SecuredField extends AbstractSecuredField {
// Add general listener for 'message' EVENT - the event that 'powers' postMessage
on(window, 'message', this.postMessageListener, false);
// TODO - only needed until latest version of 3.2.5 is on test
this.config.iframeUIConfig.ariaLabels = this.config.iframeUIConfig.ariaConfig;
// Create and send config object to iframe
const configObj: IframeConfigObject = {
fieldType: this.fieldType,
@@ -102,7 +124,6 @@ class SecuredField extends AbstractSecuredField {
extraFieldData: this.config.extraFieldData,
cardGroupTypes: this.config.cardGroupTypes,
iframeUIConfig: this.config.iframeUIConfig,
pmConfig: this.config.iframeUIConfig, // TODO - only needed until latest version of 3.2.2 is on test
sfLogAtStart: this.config.sfLogAtStart,
showWarnings: this.config.showWarnings,
trimTrailingSeparator: this.config.trimTrailingSeparator,

View File

@@ -181,7 +181,7 @@ export function setupSecuredField(pItem: HTMLElement): void {
holderEl: pItem
};
const sf: SecuredField = new SecuredField(setupObj)
const sf: SecuredField = new SecuredField(setupObj, this.props.i18n)
.onIframeLoaded((): void => {
// Count
this.state.iframeCount += 1;

View File

@@ -0,0 +1,38 @@
import { CSF_FIELDS_ARRAY, IFRAME_TITLE } from '../../../configuration/constants';
import getProp from '../../../../../../../utils/getProp';
import { addErrorTranslationToObject } from '../../../../utils';
import { AriaConfigObject, AriaConfig } from '../../AbstractSecuredField';
/**
* Checks if the merchant has defined an ariaConfig object and if so enhances it with a iframeTitle and error property, if they don't already exist
* If the ariaConfig object doesn't exist then creates one with the default properties
* In both cases where the error property doesn't exist we extract the correct translation for the field and use that
*/
export function processAriaConfig(configObj, fieldType, i18n) {
const iframeTitle: string = IFRAME_TITLE;
let newAriaFieldConfigObj: AriaConfigObject;
// Check for a pre-existing, merchant defined object
const ariaFieldConfig: AriaConfigObject = getProp(configObj, `iframeUIConfig.ariaConfig.${fieldType}`);
if (ariaFieldConfig) {
newAriaFieldConfigObj = {
...ariaFieldConfig,
// If object already has a title, use it - else set default
iframeTitle: ariaFieldConfig.iframeTitle || iframeTitle
};
} else {
// Create a new object with the default title
newAriaFieldConfigObj = { iframeTitle };
}
// Add error translation
const ariaFieldConfigWithTranslation = addErrorTranslationToObject(newAriaFieldConfigObj, fieldType, i18n, CSF_FIELDS_ARRAY);
// Create a new aria config object keeping the old entries and adding a new one for this field
// N.B. need to do this deconstruction of the original aria config object to break existing refs & avoid getting an "accumulated" object
return {
...configObj.iframeUIConfig.ariaConfig,
[fieldType]: ariaFieldConfigWithTranslation
} as AriaConfig;
}

View File

@@ -0,0 +1,20 @@
import getProp from '../../../../../../../utils/getProp';
import { resolvePlaceholders } from '../../../../utils';
import { PlaceholdersObject } from '../../AbstractSecuredField';
/**
* Checks if the merchant has defined an placeholder config object and if not create one with a value from the relevant translation file
*/
export function processPlaceholders(configObj, fieldType, i18n) {
let placeholderFieldValue: string = getProp(configObj, `iframeUIConfig.placeholders.${fieldType}`);
// If no value set by merchant - get translated one
if (!placeholderFieldValue) {
placeholderFieldValue = resolvePlaceholders(i18n)[fieldType];
}
return {
...configObj.iframeUIConfig.placeholders,
[fieldType]: placeholderFieldValue
} as PlaceholdersObject;
}

View File

@@ -1,3 +1,5 @@
import Language from '../../../../language/Language';
declare global {
interface Window {
_b$dl: boolean;
@@ -33,6 +35,7 @@ export interface SetupObject extends CSFCommonProps {
clientKey: string;
rootNode: string | HTMLElement;
callbacks?: object;
i18n?: Language;
}
export interface ConfigObject extends CSFCommonProps {

View File

@@ -6,7 +6,8 @@ import {
ENCRYPTED_EXPIRY_DATE,
ENCRYPTED_EXPIRY_MONTH,
ENCRYPTED_EXPIRY_YEAR,
ENCRYPTED_SECURITY_CODE
ENCRYPTED_SECURITY_CODE,
ENCRYPTED_PWD_FIELD
} from './lib/configuration/constants';
// ROUTINES USED IN SecuredFieldsProvider.componentDidMount TO DETECT & MAP FIELD NAMES ///////////
@@ -82,36 +83,42 @@ export const getErrorObject = (fieldType, rootNode, state) => ({
});
// -- end ROUTINES USED IN SecuredFieldsProvider.showValidation -----------------------
// USED BY SecuredFieldsProvider WHEN CREATING SETUP OBJECT FOR CSF
/**
* Adds a new, translated, property e.g. "error" to the specified keys within an object
* @param originalObject - object we want to duplicate and enhance
* @param fieldNamesList - list of keys on the original object that we want to add the new property to
* @param translationsArr - an array containing translations stored under the same keys as used in the original object
* @returns a duplicate of the original object with a new property e.g. "error" added under the specified keys
* Used below in addErrorTranslationToObject (called from SecuredField.ts) AND also by handler for SecuredFieldComponent aka CustomCardComponent
*/
export const addTranslationsToObject = (originalObject, fieldNamesList, propName, translationsArr) => {
const originalKeys = Object.keys(originalObject);
export const getTranslatedErrors = (i18n = {}) => ({
[ENCRYPTED_CARD_NUMBER]: i18n.get && i18n.get('creditCard.numberField.invalid'),
[ENCRYPTED_EXPIRY_DATE]: i18n.get && i18n.get('creditCard.expiryDateField.invalid'),
[ENCRYPTED_EXPIRY_MONTH]: i18n.get && i18n.get('creditCard.expiryDateField.invalid'),
[ENCRYPTED_EXPIRY_YEAR]: i18n.get && i18n.get('creditCard.expiryDateField.invalid'),
[ENCRYPTED_SECURITY_CODE]: i18n.get && i18n.get('creditCard.oneClickVerification.invalidInput.title'),
defaultError: 'error.title'
});
const nuObj = { ...originalObject };
originalKeys
.filter(key => fieldNamesList.includes(key))
.map(key => {
nuObj[key][propName] = !nuObj[key][propName] ? translationsArr[key] : nuObj[key][propName];
return null;
});
return nuObj;
/**
* Adds a new, translated, error property to an object, unless it already exists
* @param originalObject - object we want to duplicate and enhance
* @param key - fieldID eg. "encryptedCardNumber", id under which we expect a translation to exist
* @param i18n - an i18n object to use to get translations
* @param fieldNamesList - list of keys (fieldIDs) we want to add translations for
* @returns a duplicate of the original object with a new property: "error" whose value is a translation extracted from the i18n object
*/
export const addErrorTranslationToObject = (originalObj, key, i18n, fieldNamesList) => {
if (fieldNamesList.includes(key)) {
const nuObj = { ...originalObj };
const translatedErrors = getTranslatedErrors(i18n);
nuObj.error = !nuObj.error ? translatedErrors[key] : nuObj.error;
return nuObj;
}
return originalObj;
};
export const resolvePlaceholders = (i18n = {}) => ({
encryptedCardNumber: i18n.get && i18n.get('creditCard.numberField.placeholder'),
encryptedExpiryDate: i18n.get && i18n.get('creditCard.expiryDateField.placeholder'),
encryptedSecurityCode: i18n.get && i18n.get('creditCard.cvcField.placeholder'),
encryptedPassword: i18n.get && i18n.get('creditCard.encryptedPassword.placeholder')
[ENCRYPTED_CARD_NUMBER]: i18n.get && i18n.get('creditCard.numberField.placeholder'),
[ENCRYPTED_EXPIRY_DATE]: i18n.get && i18n.get('creditCard.expiryDateField.placeholder'),
[ENCRYPTED_SECURITY_CODE]: i18n.get && i18n.get('creditCard.cvcField.placeholder'),
[ENCRYPTED_PWD_FIELD]: i18n.get && i18n.get('creditCard.encryptedPassword.placeholder')
});
// -- end USED BY SecuredFieldsProvider WHEN CREATING SETUP OBJECT FOR CSF
/**
* Used by SecuredFieldsProviderHandlers
@@ -128,18 +135,6 @@ export const getCardImageUrl = (brand, loadingContext) => {
return getImageUrl(imageOptions)(type);
};
/**
* Used by SecuredFieldsProvider when creating setup object for csf AND also by handler for SecuredFieldComponent aka CustomCardComponent
*/
export const getTranslatedErrors = (i18n = {}) => ({
[ENCRYPTED_CARD_NUMBER]: i18n.get && i18n.get('creditCard.numberField.invalid'),
[ENCRYPTED_EXPIRY_DATE]: i18n.get && i18n.get('creditCard.expiryDateField.invalid'),
[ENCRYPTED_EXPIRY_MONTH]: i18n.get && i18n.get('creditCard.expiryDateField.invalid'),
[ENCRYPTED_EXPIRY_YEAR]: i18n.get && i18n.get('creditCard.expiryDateField.invalid'),
[ENCRYPTED_SECURITY_CODE]: i18n.get && i18n.get('creditCard.oneClickVerification.invalidInput.title'),
defaultError: 'error.title'
});
// REGULAR "UTIL" UTILS
/**
* Checks if `prop` is classified as an `Array` primitive or object.