/* eslint-disable max-len, no-useless-escape, */ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * DS103: Rewrite code to no longer use __guard__ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ import App from '../base' const defaultFormat = /(\d{1,4})/g const defaultInputFormat = /(?:^|\s)(\d{4})$/ const cards = [ // Credit cards { type: 'visa', patterns: [4], format: defaultFormat, length: [13, 16], cvcLength: [3], luhn: true }, { type: 'mastercard', patterns: [51, 52, 53, 54, 55, 22, 23, 24, 25, 26, 27], format: defaultFormat, length: [16], cvcLength: [3], luhn: true }, { type: 'amex', patterns: [34, 37], format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/, length: [15], cvcLength: [3, 4], luhn: true }, { type: 'dinersclub', patterns: [30, 36, 38, 39], format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/, length: [14], cvcLength: [3], luhn: true }, { type: 'discover', patterns: [60, 64, 65, 622], format: defaultFormat, length: [16], cvcLength: [3], luhn: true }, { type: 'unionpay', patterns: [62, 88], format: defaultFormat, length: [16, 17, 18, 19], cvcLength: [3], luhn: false }, { type: 'jcb', patterns: [35], format: defaultFormat, length: [16], cvcLength: [3], luhn: true } ] App.factory('ccUtils', function() { const cardFromNumber = function(num) { num = (num + '').replace(/\D/g, '') for (let card of Array.from(cards)) { for (let pattern of Array.from(card.patterns)) { const p = pattern + '' if (num.substr(0, p.length) === p) { return card } } } } const cardFromType = function(type) { for (let card of Array.from(cards)) { if (card.type === type) { return card } } } const cardType = function(num) { if (!num) { return null } return __guard__(cardFromNumber(num), x => x.type) || null } const formatCardNumber = function(num) { num = num.replace(/\D/g, '') const card = cardFromNumber(num) if (!card) { return num } const upperLength = card.length[card.length.length - 1] num = num.slice(0, upperLength) if (card.format.global) { return __guard__(num.match(card.format), x => x.join(' ')) } else { let groups = card.format.exec(num) if (groups == null) { return } groups.shift() groups = $.grep(groups, n => n) // Filter empty groups return groups.join(' ') } } const formatExpiry = function(expiry) { const parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/) if (!parts) { return '' } let mon = parts[1] || '' let sep = parts[2] || '' const year = parts[3] || '' if (year.length > 0) { sep = ' / ' } else if (sep === ' /') { mon = mon.substring(0, 1) sep = '' } else if (mon.length === 2 || sep.length > 0) { sep = ' / ' } else if (mon.length === 1 && !['0', '1'].includes(mon)) { mon = `0${mon}` sep = ' / ' } return mon + sep + year } const parseExpiry = function(value) { if (value == null) { value = '' } let [month, year] = Array.from(value.split(/[\s\/]+/, 2)) // Allow for year shortcut if ((year != null ? year.length : undefined) === 2 && /^\d+$/.test(year)) { let prefix = new Date().getFullYear() prefix = prefix.toString().slice(0, 2) year = prefix + year } month = parseInt(month, 10) year = parseInt(year, 10) if (!!isNaN(month) || !!isNaN(year)) { return } return { month, year } } return { fromNumber: cardFromNumber, fromType: cardFromType, cardType, formatExpiry, formatCardNumber, defaultFormat, defaultInputFormat, parseExpiry } }) App.factory('ccFormat', function(ccUtils, $filter) { const hasTextSelected = function($target) { // If some text is selected if ( $target.prop('selectionStart') != null && $target.prop('selectionStart') !== $target.prop('selectionEnd') ) { return true } // If some text is selected in IE if ( __guard__( typeof document !== 'undefined' && document !== null ? document.selection : undefined, x => x.createRange ) != null ) { if (document.selection.createRange().text) { return true } } return false } const safeVal = function(value, $target) { let cursor try { cursor = $target.prop('selectionStart') } catch (error) { cursor = null } const last = $target.val() $target.val(value) if (cursor !== null && $target.is(':focus')) { if (cursor === last.length) { cursor = value.length } // This hack looks for scenarios where we are changing an input's value such // that "X| " is replaced with " |X" (where "|" is the cursor). In those // scenarios, we want " X|". // // For example: // 1. Input field has value "4444| " // 2. User types "1" // 3. Input field has value "44441| " // 4. Reformatter changes it to "4444 |1" // 5. By incrementing the cursor, we make it "4444 1|" // // This is awful, and ideally doesn't go here, but given the current design // of the system there does not appear to be a better solution. // // Note that we can't just detect when the cursor-1 is " ", because that // would incorrectly increment the cursor when backspacing, e.g. pressing // backspace in this scenario: "4444 1|234 5". if (last !== value) { const prevPair = last.slice(cursor - 1, +cursor + 1 || undefined) const currPair = value.slice(cursor - 1, +cursor + 1 || undefined) const digit = value[cursor] if ( /\d/.test(digit) && prevPair === `${digit} ` && currPair === ` ${digit}` ) { cursor = cursor + 1 } } $target.prop('selectionStart', cursor) return $target.prop('selectionEnd', cursor) } } // Replace Full-Width Chars const replaceFullWidthChars = function(str) { if (str == null) { str = '' } const fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19' const halfWidth = '0123456789' let value = '' const chars = str.split('') // Avoid using reserved word `char` for (let chr of Array.from(chars)) { const idx = fullWidth.indexOf(chr) if (idx > -1) { chr = halfWidth[idx] } value += chr } return value } // Format Numeric const reFormatNumeric = function(e) { const $target = $(e.currentTarget) return setTimeout(function() { let value = $target.val() value = replaceFullWidthChars(value) value = value.replace(/\D/g, '') return safeVal(value, $target) }) } // Format Card Number const reFormatCardNumber = function(e) { const $target = $(e.currentTarget) return setTimeout(function() { let value = $target.val() value = replaceFullWidthChars(value) value = ccUtils.formatCardNumber(value) return safeVal(value, $target) }) } const formatCardNumber = function(e) { // Only format if input is a number let re const digit = String.fromCharCode(e.which) if (!/^\d+$/.test(digit)) { return } const $target = $(e.currentTarget) const value = $target.val() const card = ccUtils.fromNumber(value + digit) const { length } = value.replace(/\D/g, '') + digit let upperLength = 16 if (card) { upperLength = card.length[card.length.length - 1] } if (length >= upperLength) { return } // Return if focus isn't at the end of the text if ( $target.prop('selectionStart') != null && $target.prop('selectionStart') !== value.length ) { return } if (card && card.type === 'amex') { // AMEX cards are formatted differently re = /^(\d{4}|\d{4}\s\d{6})$/ } else { re = /(?:^|\s)(\d{4})$/ } // If '4242' + 4 if (re.test(value)) { e.preventDefault() return setTimeout(() => $target.val(value + ' ' + digit)) // If '424' + 2 } else if (re.test(value + digit)) { e.preventDefault() return setTimeout(() => $target.val(value + digit + ' ')) } } const formatBackCardNumber = function(e) { const $target = $(e.currentTarget) const value = $target.val() // Return unless backspacing if (e.which !== 8) { return } // Return if focus isn't at the end of the text if ( $target.prop('selectionStart') != null && $target.prop('selectionStart') !== value.length ) { return } // Remove the digit + trailing space if (/\d\s$/.test(value)) { e.preventDefault() return setTimeout(() => $target.val(value.replace(/\d\s$/, ''))) // Remove digit if ends in space + digit } else if (/\s\d?$/.test(value)) { e.preventDefault() return setTimeout(() => $target.val(value.replace(/\d$/, ''))) } } const getFormattedCardNumber = function(num) { num = num.replace(/\D/g, '') const card = ccUtils.fromNumber(num) if (!card) { return num } const upperLength = card.length[card.length.length - 1] num = num.slice(0, upperLength) if (card.format.global) { return __guard__(num.match(card.format), x => x.join(' ')) } else { let groups = card.format.exec(num) if (groups == null) { return } groups.shift() groups = $.grep(groups, n => n) // Filter empty groups return groups.join(' ') } } const parseCardNumber = function(value) { if (value != null) { return value.replace(/\s/g, '') } else { return value } } // Format Expiry const reFormatExpiry = function(e) { const $target = $(e.currentTarget) return setTimeout(function() { let value = $target.val() value = replaceFullWidthChars(value) value = ccUtils.formatExpiry(value) return safeVal(value, $target) }) } const formatExpiry = function(e) { // Only format if input is a number const digit = String.fromCharCode(e.which) if (!/^\d+$/.test(digit)) { return } const $target = $(e.currentTarget) const val = $target.val() + digit if (/^\d$/.test(val) && !['0', '1'].includes(val)) { e.preventDefault() return setTimeout(() => $target.val(`0${val} / `)) } else if (/^\d\d$/.test(val)) { e.preventDefault() return setTimeout(function() { // Split for months where we have the second digit > 2 (past 12) and turn // that into (m1)(m2) => 0(m1) / (m2) const m1 = parseInt(val[0], 10) const m2 = parseInt(val[1], 10) if (m2 > 2 && m1 !== 0) { return $target.val(`0${m1} / ${m2}`) } else { return $target.val(`${val} / `) } }) } } const formatForwardExpiry = function(e) { const digit = String.fromCharCode(e.which) if (!/^\d+$/.test(digit)) { return } const $target = $(e.currentTarget) const val = $target.val() if (/^\d\d$/.test(val)) { return $target.val(`${val} / `) } } const formatForwardSlash = function(e) { const which = String.fromCharCode(e.which) if (which !== '/' && which !== ' ') { return } const $target = $(e.currentTarget) const val = $target.val() if (/^\d$/.test(val) && val !== '0') { return $target.val(`0${val} / `) } } const formatBackExpiry = function(e) { const $target = $(e.currentTarget) const value = $target.val() // Return unless backspacing if (e.which !== 8) { return } // Return if focus isn't at the end of the text if ( $target.prop('selectionStart') != null && $target.prop('selectionStart') !== value.length ) { return } // Remove the trailing space + last digit if (/\d\s\/\s$/.test(value)) { e.preventDefault() return setTimeout(() => $target.val(value.replace(/\d\s\/\s$/, ''))) } } const parseExpiry = function(value) { if (value != null) { const dateAsObj = ccUtils.parseExpiry(value) if (dateAsObj == null) { return } const expiry = new Date(dateAsObj.year, dateAsObj.month - 1) return $filter('date')(expiry, 'MM/yyyy') } } // Format CVC const reFormatCVC = function(e) { const $target = $(e.currentTarget) return setTimeout(function() { let value = $target.val() value = replaceFullWidthChars(value) value = value.replace(/\D/g, '').slice(0, 4) return safeVal(value, $target) }) } // Restrictions const restrictNumeric = function(e) { // Key event is for a browser shortcut if (e.metaKey || e.ctrlKey) { return true } // If keycode is a space if (e.which === 32) { return false } // If keycode is a special char (WebKit) if (e.which === 0) { return true } // If char is a special char (Firefox) if (e.which < 33) { return true } const input = String.fromCharCode(e.which) // Char is a number or a space return !!/[\d\s]/.test(input) } const restrictCardNumber = function(e) { const $target = $(e.currentTarget) const digit = String.fromCharCode(e.which) if (!/^\d+$/.test(digit)) { return } if (hasTextSelected($target)) { return } // Restrict number of digits const value = ($target.val() + digit).replace(/\D/g, '') const card = ccUtils.fromNumber(value) if (card) { return value.length <= card.length[card.length.length - 1] } else { // All other cards are 16 digits long return value.length <= 16 } } const restrictExpiry = function(e) { const $target = $(e.currentTarget) const digit = String.fromCharCode(e.which) if (!/^\d+$/.test(digit)) { return } if (hasTextSelected($target)) { return } let value = $target.val() + digit value = value.replace(/\D/g, '') if (value.length > 6) { return false } } const restrictCVC = function(e) { const $target = $(e.currentTarget) const digit = String.fromCharCode(e.which) if (!/^\d+$/.test(digit)) { return } if (hasTextSelected($target)) { return } const val = $target.val() + digit return val.length <= 4 } const setCardType = function(e) { const $target = $(e.currentTarget) const val = $target.val() const cardType = ccUtils.cardType(val) || 'unknown' if (!$target.hasClass(cardType)) { const allTypes = Array.from(cards).map(card => card.type) $target.removeClass('unknown') $target.removeClass(allTypes.join(' ')) $target.addClass(cardType) $target.toggleClass('identified', cardType !== 'unknown') return $target.trigger('payment.cardType', cardType) } } return { hasTextSelected, replaceFullWidthChars, reFormatNumeric, reFormatCardNumber, formatCardNumber, formatBackCardNumber, getFormattedCardNumber, parseCardNumber, reFormatExpiry, formatExpiry, formatForwardExpiry, formatForwardSlash, formatBackExpiry, parseExpiry, reFormatCVC, restrictNumeric, restrictCardNumber, restrictExpiry, restrictCVC, setCardType } }) App.directive('ccFormatExpiry', ccFormat => ({ restrict: 'A', require: 'ngModel', link(scope, el, attrs, ngModel) { el.on('keypress', ccFormat.restrictNumeric) el.on('keypress', ccFormat.restrictExpiry) el.on('keypress', ccFormat.formatExpiry) el.on('keypress', ccFormat.formatForwardSlash) el.on('keypress', ccFormat.formatForwardExpiry) el.on('keydown', ccFormat.formatBackExpiry) el.on('change', ccFormat.reFormatExpiry) el.on('input', ccFormat.reFormatExpiry) el.on('paste', ccFormat.reFormatExpiry) ngModel.$parsers.push(ccFormat.parseExpiry) return ngModel.$formatters.push(ccFormat.parseExpiry) } })) App.directive('ccFormatCardNumber', ccFormat => ({ restrict: 'A', require: 'ngModel', link(scope, el, attrs, ngModel) { el.on('keypress', ccFormat.restrictNumeric) el.on('keypress', ccFormat.restrictCardNumber) el.on('keypress', ccFormat.formatCardNumber) el.on('keydown', ccFormat.formatBackCardNumber) el.on('paste', ccFormat.reFormatCardNumber) ngModel.$parsers.push(ccFormat.parseCardNumber) return ngModel.$formatters.push(ccFormat.getFormattedCardNumber) } })) export default App.directive('ccFormatSecCode', ccFormat => ({ restrict: 'A', require: 'ngModel', link(scope, el, attrs, ngModel) { el.on('keypress', ccFormat.restrictNumeric) el.on('keypress', ccFormat.restrictCVC) el.on('paste', ccFormat.reFormatCVC) el.on('change', ccFormat.reFormatCVC) return el.on('input', ccFormat.reFormatCVC) } })) function __guard__(value, transform) { return typeof value !== 'undefined' && value !== null ? transform(value) : undefined }