import templateStyles from './cardTemplateStyles'
/**
*
* @type {{setCCHtml: (function(*): *), apiKey: string, getNameValidity: (function(): boolean), getExpirationValidity: (function(): boolean), visaRegex: RegExp, masterCardRegex: RegExp, getApiUrl: (function(): string), apiUrl: string, setExpirationValidity: (function(*): *), cvvValid: boolean, dinersRegex: RegExp, getApiKey: (function(): string), readENVTextFile: (function(String): Promise<unknown>), zipCodeRegex: RegExp, setEnv: Window.StackPay.setEnv, setAPIUrl: (function(*): *), ccHtml: string, getCvvValidity: (function(): boolean), setZipCodeValidity: (function(*): *), nameRegex: RegExp, amexRegex: RegExp, nameValid: boolean, getCardValidity: (function(): boolean), zipCodeValid: boolean, getCardType: (function(): string), fetchHtml: (function(string): Promise<HTMLMarkup>), expirationValid: boolean, s3Url: string, cardIsValid: boolean, setCvvValidity: (function(*): *), monthYearRegex: RegExp, setApiKey: (function(string): Promise<String>), verifyKey: (function(string, string): Promise<void>), jcbRegex: RegExp, setCardValidity: (function(*): *), getCCHtml: (function(): string), cvvAmex: RegExp, appendCardHtml: (function(string): Promise<HTMLObjectElement>), cvvRegex: RegExp, cardType: string, discoverRegex: RegExp, getPaymentMethodToken: (function(Object): Promise<Object>), env: string, getZipCodeValidity: (function(): boolean), getS3Url: (function(): string), setS3Url: (function(*): *), serializeFormData: (function(Object): {zipcode: *, cvv: *, name: *, cardType: *, expiration: *, cardNumber: *}), elements: {createAchElement: (function(): Promise<HTMLObjectElement>), validateZipCode: (function(string): Promise<Boolean>), validateVisa: (function(string): boolean), expirationValidDigits: (function(string): *), validateMasterCard: (function(string): boolean), validateJCB: (function(string): boolean), validateName: (function(string): Promise<Boolean>), validateDinersCard: (function(string): boolean), validateCardNumber: (function(string): Promise<Object>), validateCVV: (function(string, string): Promise<Boolean>), determineFormValidity: (function(): *), create: (function(string, Object=): Promise<void>), validateDiscover: (function(string): boolean), validateCardExpiration: (function(string): Promise<unknown>), validateAmex: (function(string): boolean), createCardElement: (function(): Promise<HTMLObjectElement>)}, setCardType: (function(*): *), setNameValidity: (function(*): *)}}
*/
export class StackPay {
constructor() {
/**
* the API Public Key furnished by Stack Pay
* @type {string}
*/
this.apiKey = ""
/**
* The environment local, sandbox, or production
* @type {string}
*/
this.env = ''
/**
* S3 bucket URL
* @type {string}
*/
this.s3Url = ''
/**
* URL for Stack Pay API
* @type {string}
*/
this.apiUrl = ''
/**
* Contains the HTML for the card template
* @type {string}
*/
this.ccHtml = ''
/**
* Variable containing Card Type
* String containing type of card being passed into the form
* @type {string}
*/
this.cardType = ''
/**
* Boolean that receives the name validation result
* @type {boolean}
*/
this.nameValid = false
/**
* Boolean that receives the card validation result
* @type {boolean}
*/
this.cardIsValid = false
/**
* Boolean that receives the card expiration validation result
* @type {boolean}
*/
this.expirationValid = false
/**
* Boolean that receives the CVV validation result
* @type {boolean}
*/
this.cvvValid = false
/**
* Boolean that receives the Zip Code validation result
* @type {boolean}
*/
this.zipCodeValid = false
/************** INPUT REGEX STRINGS ****************/
/**
* Regex that validates user first and last name
* @type {RegExp}
*/
this.nameRegex= /^[a-zA-Z]{2 }(?!.*--)[a-zA-Z- ]*[a-zA-Z]$/i
/**
* Regex that validates expiry date
* Further validation exists in validateCardExpiration()
* @type {RegExp}
*/
this.monthYearRegex= /^\d\d\d\d$/
/**
* Regex that validates CVV
* @type {RegExp}
*/
this.cvvRegex= /^[0-9]{3}$/
/**
* Regex that validates CVV for Amex cards
* @type {RegExp}
*/
this.cvvAmex= /^[0-9]{4}$/
/**
* Regex that validates Zip Codes
* @type {RegExp}
*/
this.zipCodeRegex= /^\d{5}$|^\d{5}-\d{4}$/
/**
* Regex that validates Visa cards
* @type {RegExp}
*/
this.visaRegex= /^4[0-9]{12}(?:[0-9]{3})?/
/**
* Regex that validates Mastercard
* @type {RegExp}
*/
this.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}$/
/**
* Regex that validates Amex cards
* @type {RegExp}
*/
this.amexRegex= /^3[47][0-9]{13}$/i
/**
* Regex that validates Discover cards
* @type {RegExp}
*/
this.discoverRegex= /^6(?:011|5[0-9]{2})[0-9]{12}$/
/**
* Regex that validates Diners cards
* @type {RegExp}
*/
this.dinersRegex= /^3(?:0[0-5]|[68][0-9])[0-9]{11}$/i
/**
* Regex that validates JCB cards
* @type {RegExp}
*/
this.jcbRegex= /^(?:2131|1800|35\d{3})\d{11}$/
}
/**
* 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)))
})
}
/**
* Set the S3 Bucket URL
* @param url
* @returns {string}
*/
setS3Url = (url) => {
return StackPay.s3Url = url
}
/**
* Gets the S3 Bucket URL
* @returns {string}
*/
getS3Url = () => {
return StackPay.s3Url
}
/**
* Set Stack Pay API URL
* @param apiUrl
* @returns {string}
*/
setAPIUrl = (apiUrl) => {
return StackPay.apiUrl = apiUrl
}
/**
* Returns the Stack Pay API URL
* @returns {string}
*/
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)
})
}
/**
* Returns the API Public Key that was passed into the verifyKey()
* @returns {string}
*/
getApiKey = () => {
return StackPay.apiKey
}
/**
* Set the Credit Card Template HTML in order to load the CC
* inputs into the card-element
* @param html
* @returns {HTMLElement}
*/
setCCHtml = (html) => {
return StackPay.ccHtml = html
}
/**
* Returns the Credit Card HTML inputs
* @returns {HTMLElement}
*/
getCCHtml = () => {
return StackPay.ccHtml
}
/**
* Set boolean result of name after validation
* @param validity
* @returns {boolean}
*/
setNameValidity = (validity) => {
return StackPay.nameValid = validity
}
/**
* Returns boolean result of name validity
* @returns {boolean}
*/
getNameValidity = () => {
return StackPay.nameValid
}
/**
* Sets the credit card type being entered into the form
* @param type
* @returns {string}
*/
setCardType = (type) => {
return StackPay.cardType = type
}
/**
* Returns credit card type
* @returns {string}
*/
getCardType = () => {
return StackPay.cardType
}
/**
* Sets boolean result of credit card validation
* @param validity
* @returns {boolean}
*/
setCardValidity = (validity) => {
return StackPay.cardIsValid = validity
}
/**
* Returns boolean representing whether card is valid or not
* @returns {boolean}
*/
getCardValidity = () => {
return StackPay.cardIsValid
}
/**
* Sets boolean result of expiration validity
* @param validity
* @returns {boolean}
*/
setExpirationValidity = (validity) => {
return StackPay.expirationValid = validity
}
/**
* Returns boolean result representing expiration date validity
* @returns {boolean}
*/
getExpirationValidity = () => {
return StackPay.expirationValid
}
/**
* Set boolean value representing CVV validity
* @param validity
* @returns {boolean}
*/
setCvvValidity = (validity) => {
return StackPay.cvvValid = validity
}
/**
* Returns boolean representing CVV validity
* @returns {boolean}
*/
getCvvValidity = () => {
return StackPay.cvvValid
}
/**
* Set boolean value representing zip code validity
* @param validity
* @returns {boolean}
*/
setZipCodeValidity = (validity) => {
return StackPay.zipCodeValid = validity
}
/**
* Returns boolean value representing zip code validity
* @returns {boolean}
*/
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))
})
}
/**
* 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()
}
}
export default StackPay