import templateStyles from './cardTemplateStyles'
window.StackPay = {
apiKey: "",
env: '',
s3Url: ``,
apiUrl: ``,
ccHtml: "",
cardType: '',
nameValid: false,
cardIsValid: false,
expirationValid: false,
cvvValid: false,
zipCodeValid: false,
/************** INPUT REGEX STRINGS ****************/
nameRegex: /^[a-zA-Z]{2,}(?!.*--)[a-zA-Z- ]*[a-zA-Z]$/i,
monthYearRegex: /^\d\d\d\d$/,
cvvRegex: /^[0-9]{3}$/,
cvvAmex: /^[0-9]{4}$/,
zipCodeRegex: /^\d{5}$|^\d{5}-\d{4}$/,
/************** CREDIT CARD REGEX STRINGS *******************/
visaRegex: /^4[0-9]{12}(?:[0-9]{3})?/,
masterCardRegex: /^(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}$/,
amexRegex: /^3[47][0-9]{13}$/i,
discoverRegex: /^6(?:011|5[0-9]{2})[0-9]{12}$/,
dinersRegex: /^3(?:0[0-5]|[68][0-9])[0-9]{11}$/i,
jcbRegex: /^(?:2131|1800|35\d{3})\d{11}$/,
/************** VERIFY PROVIDED PUBLIC KEY ************/
/**
* Verifies public key and delegates environment
* @param {string} publicKey - supplied by Stack Pay
* @param {string} mode - either local, sandbox, or production
* @returns {Promise<void>}
*/
verifyKey: (publicKey, mode) => {
return new Promise((resolve, reject) => {
if (publicKey === '' || publicKey === null) {
reject('Public Key is required. ');
}
StackPay.setApiKey(publicKey).then(() => resolve(StackPay.setEnv(mode)))
})
},
/************** Getters and Setters ************/
setS3Url: (url) => {
return StackPay.s3Url = url
},
getS3Url: () => {
return StackPay.s3Url
},
setAPIUrl: (apiUrl) => {
return StackPay.apiUrl = apiUrl
},
getApiUrl: () => {
return StackPay.apiUrl
},
/**
* Evaluates entered public key, and returns a promise with API Key String
* @param {string} key
* @returns {Promise<String>}
*/
setApiKey: (key) => {
return new Promise((resolve, reject) => {
StackPay.apiKey = key
resolve(StackPay.apiKey)
})
},
getApiKey: () => {
return StackPay.apiKey
},
setCCHtml: (html) => {
return StackPay.ccHtml = html
},
getCCHtml: () => {
return StackPay.ccHtml
},
/********************* Getters / Setters for VALIDITY OF INPUTS ******************/
setNameValidity: (validity) => {
return StackPay.nameValid = validity
},
getNameValidity: () => {
return StackPay.nameValid
},
setCardType: (type) => {
return StackPay.cardType = type
},
getCardType: () => {
return StackPay.cardType
},
setCardValidity: (validity) => {
return StackPay.cardIsValid = validity
},
getCardValidity: () => {
return StackPay.cardIsValid
},
setExpirationValidity: (validity) => {
return StackPay.expirationValid = validity
},
getExpirationValidity: () => {
return StackPay.expirationValid
},
setCvvValidity: (validity) => {
return StackPay.cvvValid = validity
},
getCvvValidity: () => {
return StackPay.cvvValid
},
setZipCodeValidity: (validity) => {
return StackPay.zipCodeValid = validity
},
getZipCodeValidity: () => {
return StackPay.zipCodeValid
},
/**
* Reads the env file that is designated by the mode parameter in validateKey() method.
* Resolves an object containing key & value of each line in the ENV file.
* @param {String} fileUrl
* @returns {Promise<Object>}
*/
readENVTextFile: (fileUrl) => {
return new Promise(async (resolve, reject) => {
const utf8Decoder = new TextDecoder('utf-8')
const response = await fetch(fileUrl)
const reader = response.body.getReader()
let { value: textContent, done: readerDone } = await reader.read()
textContent = textContent ? utf8Decoder.decode(textContent) : ''
// Split ENV file by line break
const re = /\n|\r|\r\n/gm
let result = re.exec(textContent).input.split("\n")
let envVariables = []
for await (let line of result) {
if (line === '') {
continue;
}
// Split each line by key / value
let splitLine = line.split('=')
envVariables.push({
key: splitLine[0],
value: splitLine[1]
})
}
resolve(envVariables)
}).catch(error => console.log('Error Reading selected ENV: ', error))
},
/**
* Sets ENV variable, and assigns the proper value to the StackPay.apiUrl
* @param {string} mode
* @returns {Promise<void>}
*/
setEnv: async (mode) => {
StackPay.env = mode
await StackPay.readENVTextFile('https://s3-us-west-2.amazonaws.com/js.stackpay.com/v1/.env.' + mode)
.then(result => {
StackPay.setS3Url(result[0].value)
StackPay.setAPIUrl(result[1].value)
})
console.log('S3 URL: ', StackPay.getS3Url())
console.log('API URL: ', StackPay.getApiUrl())
},
/**
* Fetch HTML from designated template files in order to inject them within the
* payment form
* @param {string} url
* @returns Promise<HTMLMarkup>
*/
fetchHtml: (url) => {
return new Promise(async (resolve, reject) => {
await fetch(url, {
headers: { 'Access-Control-Allow-Origin': '*' }
})
.then(response => response.text()) // convert the result to text in order to parse as HTML
.then(html => {
// Initialize the DOM parser
let parser = new DOMParser()
// Parse the text
let parsedHTML = parser.parseFromString(html, "text/html")
resolve(parsedHTML.body)
})
.catch(err => {
console.log('Failed to fetch HTML from template: ', err)
reject(err)
})
})
},
/**
*
* @param {string} containerElementId
* @returns {Promise<HTMLObjectElement>}
*/
appendCardHtml: (containerElementId) => {
return new Promise((resolve, reject) => {
let html = StackPay.getCCHtml();
if (html === '' || html === null || typeof html !== 'object') {
reject('Cannot append Card HTML to form')
}
let container = document.getElementById(containerElementId)
resolve(container.append (html))
})
},
/**
* Takes the serialized form data and returns a json object so
* that it can be consumed by getPaymentMethodToken()
* @param {object} options
* @returns {object}
*/
serializeFormData: (options) => {
return {
name: options[0]['value'],
cardNumber: options[1]['value'],
cardType: options[2]['value'],
expiration: options[3]['value'],
cvv: options[4]['value'],
zipcode: options[5]['value']
}
},
/**
* Return Payment Method Token from Stack Pay API
* @param {object} options
* @returns Promise<Object>
*/
getPaymentMethodToken: (options) => {
return new Promise((resolve, reject) => {
let data = StackPay.serializeFormData(options)
let apiKey = StackPay.getApiKey()
return fetch(StackPay.apiUrl +`/api/token`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json;charset=utf-8',
Authorization: `Bearer ${apiKey}`
},
body: JSON.stringify({
Order: {
Account: {
Type: data.cardType,
Number: data.cardNumber.replace(/\s+/g, ''),
Cvv2: data.cvv,
ExpireDate: data.expiration.replace(/\//g, '')
},
AccountHolder: {
Name: data.name,
BillingAddress: {
Address1: '1234 Payment Method Dr',
City: 'Frisco',
State: 'Texas',
Zip: data.zipcode
}
}
}
})
})
.then(result => {
if (result.status === 200) {
return result.json()
} else {
throw new Error('You are unauthorized')
}
})
.then(data => {
resolve({
hash: data.Header.Security.Hash,
hashMethod: data.Header.Security,
status: data.Body.Status === 1
? 'success'
: 'failure',
token: data.Body.Token
} )
}).catch(error => reject(error))
})
},
/**
* Method related to the input elements
*/
elements: {
/**
* Create Element
* @param {string} elType either 'card' or 'ach'
* @param {object} styles json object containing style configuration
* @returns {Promise<void>}
*/
create: (elType, styles = {}) => {
return new Promise((resolve, reject) => {
let sp = StackPay.elements
switch (elType) {
case 'card':
sp.createCardElement()
.then(card => {
StackPay.setCCHtml(card)
templateStyles(styles)
resolve()
})
break;
case 'ach':
sp.createAchElement()
.then(ach => {
StackPay.setCCHtml(
StackPay.elements.injectStyles(ach)
)
resolve()
})
break;
}
})
},
/**
* Create Card Element and fetch Required HTML
* @returns {Promise<HTMLObjectElement>}
*/
createCardElement: () => {
return new Promise((resolve, reject) => {
StackPay.fetchHtml(StackPay.getS3Url() + '/card.template.html')
.then(html => resolve(html))
.catch(error => reject(error))
})
},
/**
* Create ACH Element and fetch required HTML
* @returns {Promise<HTMLObjectElement>}
*/
createAchElement: () => {
return new Promise((resolve, reject) => {
StackPay.fetchHtml(StackPay.getS3Url() + '/ach.template.html')
.then(html => resolve(html))
.catch(error => reject(error))
})
},
/**
* Name can have letters, spaces, dashes, comma, and period, with no numbers, and no special characters.
* At least 3 letters.
* @param {string} text
* @returns {Promise<Boolean>}
*/
validateName: (text) => {
return new Promise((resolve, reject) => {
let result = StackPay.nameRegex.test(text)
// Set name validity in order to determine whether to submit the API call for Payment Token
StackPay.setNameValidity(result)
resolve(result)
})
},
/**
* Using Regex strings to validate designated card numbers
* @param {string} cardNumber
* @returns {Promise<Object>}
*/
validateCardNumber: (cardNumber) => {
return new Promise((resolve, reject) => {
if (StackPay.elements.validateVisa(cardNumber)) {
resolve({
type: 'visa',
result: true
})
} else if (StackPay.elements.validateMasterCard(cardNumber)) {
resolve({
type: 'mastercard',
result: true
})
} else if (StackPay.elements.validateAmex(cardNumber)) {
resolve({
type: 'amex',
result: true
})
} else if (StackPay.elements.validateDinersCard(cardNumber)) {
resolve({
type: 'diners',
result: true
})
} else if (StackPay.elements.validateDiscover(cardNumber)) {
resolve({
type: 'discover',
result: true
})
} else if (StackPay.elements.validateJCB(cardNumber)) {
resolve({
type: 'jcb',
result: true
})
} else {
resolve({
type: undefined,
result: false
})
}
})
},
/**
* Validates Visa cards and returns boolean
* @param {string} cardNumber
* @returns {boolean}
*/
validateVisa: (cardNumber) => {
return StackPay.visaRegex.test(cardNumber)
},
/**
* Validates MasterCard cards and returns boolean
* @param {string} cardNumber
* @returns {boolean}
*/
validateMasterCard: (cardNumber) => {
return StackPay.masterCardRegex.test(cardNumber)
},
/**
* Validates Amex cards and returns boolean
* @param {string} cardNumber
* @returns {boolean}
*/
validateAmex: (cardNumber) => {
return StackPay.amexRegex.test(cardNumber)
},
/**
* Validates Discover cards and returns boolean
* @param {string} cardNumber
* @returns {boolean}
*/
validateDiscover: (cardNumber) => {
return StackPay.discoverRegex.test(cardNumber)
},
/**
* Validates Diners cards and returns boolean
* @param {string} cardNumber
* @returns {boolean}
*/
validateDinersCard: (cardNumber) => {
return StackPay.dinersRegex.test(cardNumber)
},
/**
* Validates JCB cards and returns boolean
* @param {string} cardNumber
* @returns {boolean}
*/
validateJCB: (cardNumber) => {
return StackPay.jcbRegex.test(cardNumber)
},
/**
* Expiration date validation
* month must be between 1 & 12 inclusive
* year must be greater than current year
* Month must be greater than current month if entered year === current year
* Entered Month can be less than current month as long as Entered Year is greater than Current Year *
* @param {string} expiration
* @returns {Promise<Boolean>}
*/
validateCardExpiration: (expiration) => {
return new Promise((resolve, reject) => {
const extracted = () => {
// check that month & year are digits
let validated = StackPay.elements.expirationValidDigits(expiration)
// obtain the entered month
let enteredMonth = validated.valid
? parseInt(validated.result.substring(0, 2), 10)
: 0
let currentMonth = new Date().getMonth()
// obtain entered year
let enteredYear = parseInt('20' + validated.result.substring(2, 4), 10)
let currentYear = new Date().getFullYear()
// if Entered Year > Current Year, then Entered Month === Current Month
let sameMonthGreaterEnteredYear = enteredYear > currentYear && enteredMonth === currentMonth
// if Entered Year > Current Year && Entered Month < Current Month
let greaterEnteredYearLesserEnteredMonth = enteredYear > currentYear && enteredMonth < currentMonth
// If Entered Year >= Current Year, then Entered Month > Current Month
let greaterEnteredYearGreaterMonth = enteredYear >= currentYear && enteredMonth > currentMonth
// check that month is between 1 & 12 inclusive
let monthBetweenOneAndTwelve = enteredMonth >= 1 && enteredMonth <= 12
let result = sameMonthGreaterEnteredYear ||
greaterEnteredYearLesserEnteredMonth ||
greaterEnteredYearGreaterMonth &&
monthBetweenOneAndTwelve
return result;
}
let result = extracted();
// Set validity of entered expiration in order to determine whether to submit API call
StackPay.setExpirationValidity(result)
resolve(result)
}).catch(error => console.log('Caught Exception validateCardExpiration(): ', error))
},
/**
* Expiration Date must be 4 numerical digits
* @param {string} expiration
* @returns {Object}
*/
expirationValidDigits: (expiration) => {
return StackPay.monthYearRegex.test(expiration)
? {
result: expiration,
valid: true
}
: {
result: null,
valid: false
}
},
/**
* CVV Must be either 3 or 4 (if AMEX ) numerical digits
* @param {string} cvv
* @param {string} cardType
* @returns {Promise<Boolean>}
*/
validateCVV: (cvv, cardType) => {
let validated;
return new Promise((resolve, reject) => {
let cardTypeExists = cardType !== '' && cardType !== null
let cvvExists = cvv !== '' && cvv !== null
let exists = cvvExists && cardTypeExists
if (!exists) {
reject('Missing CVV & Card Type')
}
validated = cardType === 'amex'
? StackPay.cvvAmex.test(cvv)
: StackPay.cvvRegex.test(cvv)
// Set CVV Validity in order to determine whether to submit API call for payment token
StackPay.setCvvValidity(validated)
resolve(validated)
})
},
/**
* Determine validity of zip code using regex string
* Sets also zipCodeValid variable.
* @param {string} zipcode
* @returns {Promise<Boolean>}
*/
validateZipCode: (zipcode) => {
return new Promise((resolve, reject) => {
// Set Zip Code Validity in order to determine whether to submit API call for payment token
StackPay.setZipCodeValidity(StackPay.zipCodeRegex.test(zipcode))
resolve(StackPay.zipCodeRegex.test(zipcode))
})
},
/**
* Method to use within the Payment Method Token Form
* This determines whether we can submit the form.
* If all inputs are invalid, then the form must not be submitted
* @returns {boolean}
*/
determineFormValidity: () => {
return StackPay.getNameValidity() &&
StackPay.getCardValidity() &&
StackPay.getExpirationValidity() &&
StackPay.getCvvValidity() &&
StackPay.getZipCodeValidity()
}
},
}