This repository has been archived by the owner. It is now read-only.
Permalink
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up| $ = window.jQuery or window.Zepto or window.$ | |
| $.payment = {} | |
| $.payment.fn = {} | |
| $.fn.payment = (method, args...) -> | |
| $.payment.fn[method].apply(this, args) | |
| # Utils | |
| defaultFormat = /(\d{1,4})/g | |
| $.payment.cards = cards = [ | |
| { | |
| type: 'maestro' | |
| patterns: [ | |
| 5018, 502, 503, 506, 56, 58, 639, 6220, 67 | |
| ] | |
| format: defaultFormat | |
| length: [12..19] | |
| cvcLength: [3] | |
| luhn: true | |
| } | |
| { | |
| type: 'forbrugsforeningen' | |
| patterns: [600] | |
| format: defaultFormat | |
| length: [16] | |
| cvcLength: [3] | |
| luhn: true | |
| } | |
| { | |
| type: 'dankort' | |
| patterns: [5019] | |
| format: defaultFormat | |
| length: [16] | |
| cvcLength: [3] | |
| luhn: true | |
| } | |
| # 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..19] | |
| cvcLength: [3] | |
| luhn: false | |
| } | |
| { | |
| type: 'jcb' | |
| patterns: [35] | |
| format: defaultFormat | |
| length: [16] | |
| cvcLength: [3] | |
| luhn: true | |
| } | |
| ] | |
| cardFromNumber = (num) -> | |
| num = (num + '').replace(/\D/g, '') | |
| for card in cards | |
| for pattern in card.patterns | |
| p = pattern + '' | |
| return card if num.substr(0, p.length) == p | |
| cardFromType = (type) -> | |
| return card for card in cards when card.type is type | |
| luhnCheck = (num) -> | |
| odd = true | |
| sum = 0 | |
| digits = (num + '').split('').reverse() | |
| for digit in digits | |
| digit = parseInt(digit, 10) | |
| digit *= 2 if (odd = !odd) | |
| digit -= 9 if digit > 9 | |
| sum += digit | |
| sum % 10 == 0 | |
| hasTextSelected = ($target) -> | |
| # If some text is selected | |
| return true if $target.prop('selectionStart')? and | |
| $target.prop('selectionStart') isnt $target.prop('selectionEnd') | |
| # If some text is selected in IE | |
| if document?.selection?.createRange? | |
| return true if document.selection.createRange().text | |
| false | |
| # Private | |
| # Safe Val | |
| safeVal = (value, $target) -> | |
| try | |
| cursor = $target.prop('selectionStart') | |
| catch error | |
| cursor = null | |
| last = $target.val() | |
| $target.val(value) | |
| if cursor != null && $target.is(":focus") | |
| cursor = value.length if cursor is last.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 | |
| prevPair = last[cursor-1..cursor] | |
| currPair = value[cursor-1..cursor] | |
| digit = value[cursor] | |
| cursor = cursor + 1 if /\d/.test(digit) and | |
| prevPair == "#{digit} " and currPair == " #{digit}" | |
| $target.prop('selectionStart', cursor) | |
| $target.prop('selectionEnd', cursor) | |
| # Replace Full-Width Chars | |
| replaceFullWidthChars = (str = '') -> | |
| fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19' | |
| halfWidth = '0123456789' | |
| value = '' | |
| chars = str.split('') | |
| # Avoid using reserved word `char` | |
| for chr in chars | |
| idx = fullWidth.indexOf(chr) | |
| chr = halfWidth[idx] if idx > -1 | |
| value += chr | |
| value | |
| # Format Numeric | |
| reFormatNumeric = (e) -> | |
| $target = $(e.currentTarget) | |
| setTimeout -> | |
| value = $target.val() | |
| value = replaceFullWidthChars(value) | |
| value = value.replace(/\D/g, '') | |
| safeVal(value, $target) | |
| # Format Card Number | |
| reFormatCardNumber = (e) -> | |
| $target = $(e.currentTarget) | |
| setTimeout -> | |
| value = $target.val() | |
| value = replaceFullWidthChars(value) | |
| value = $.payment.formatCardNumber(value) | |
| safeVal(value, $target) | |
| formatCardNumber = (e) -> | |
| # Only format if input is a number | |
| digit = String.fromCharCode(e.which) | |
| return unless /^\d+$/.test(digit) | |
| $target = $(e.currentTarget) | |
| value = $target.val() | |
| card = cardFromNumber(value + digit) | |
| length = (value.replace(/\D/g, '') + digit).length | |
| upperLength = 16 | |
| upperLength = card.length[card.length.length - 1] if card | |
| return if length >= upperLength | |
| # Return if focus isn't at the end of the text | |
| return if $target.prop('selectionStart')? and | |
| $target.prop('selectionStart') isnt value.length | |
| if card && card.type is '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() | |
| setTimeout -> $target.val(value + ' ' + digit) | |
| # If '424' + 2 | |
| else if re.test(value + digit) | |
| e.preventDefault() | |
| setTimeout -> $target.val(value + digit + ' ') | |
| formatBackCardNumber = (e) -> | |
| $target = $(e.currentTarget) | |
| value = $target.val() | |
| # Return unless backspacing | |
| return unless e.which is 8 | |
| # Return if focus isn't at the end of the text | |
| return if $target.prop('selectionStart')? and | |
| $target.prop('selectionStart') isnt value.length | |
| # Remove the digit + trailing space | |
| if /\d\s$/.test(value) | |
| e.preventDefault() | |
| setTimeout -> $target.val(value.replace(/\d\s$/, '')) | |
| # Remove digit if ends in space + digit | |
| else if /\s\d?$/.test(value) | |
| e.preventDefault() | |
| setTimeout -> $target.val(value.replace(/\d$/, '')) | |
| # Format Expiry | |
| reFormatExpiry = (e) -> | |
| $target = $(e.currentTarget) | |
| setTimeout -> | |
| value = $target.val() | |
| value = replaceFullWidthChars(value) | |
| value = $.payment.formatExpiry(value) | |
| safeVal(value, $target) | |
| formatExpiry = (e) -> | |
| # Only format if input is a number | |
| digit = String.fromCharCode(e.which) | |
| return unless /^\d+$/.test(digit) | |
| $target = $(e.currentTarget) | |
| val = $target.val() + digit | |
| if /^\d$/.test(val) and val not in ['0', '1'] | |
| e.preventDefault() | |
| setTimeout -> $target.val("0#{val} / ") | |
| else if /^\d\d$/.test(val) | |
| e.preventDefault() | |
| setTimeout -> | |
| # Split for months where we have the second digit > 2 (past 12) and turn | |
| # that into (m1)(m2) => 0(m1) / (m2) | |
| m1 = parseInt(val[0], 10) | |
| m2 = parseInt(val[1], 10) | |
| if m2 > 2 and m1 != 0 | |
| $target.val("0#{m1} / #{m2}") | |
| else | |
| $target.val("#{val} / ") | |
| formatForwardExpiry = (e) -> | |
| digit = String.fromCharCode(e.which) | |
| return unless /^\d+$/.test(digit) | |
| $target = $(e.currentTarget) | |
| val = $target.val() | |
| if /^\d\d$/.test(val) | |
| $target.val("#{val} / ") | |
| formatForwardSlashAndSpace = (e) -> | |
| which = String.fromCharCode(e.which) | |
| return unless which is '/' or which is ' ' | |
| $target = $(e.currentTarget) | |
| val = $target.val() | |
| if /^\d$/.test(val) and val isnt '0' | |
| $target.val("0#{val} / ") | |
| formatBackExpiry = (e) -> | |
| $target = $(e.currentTarget) | |
| value = $target.val() | |
| # Return unless backspacing | |
| return unless e.which is 8 | |
| # Return if focus isn't at the end of the text | |
| return if $target.prop('selectionStart')? and | |
| $target.prop('selectionStart') isnt value.length | |
| # Remove the trailing space + last digit | |
| if /\d\s\/\s$/.test(value) | |
| e.preventDefault() | |
| setTimeout -> $target.val(value.replace(/\d\s\/\s$/, '')) | |
| # Format CVC | |
| reFormatCVC = (e) -> | |
| $target = $(e.currentTarget) | |
| setTimeout -> | |
| value = $target.val() | |
| value = replaceFullWidthChars(value) | |
| value = value.replace(/\D/g, '')[0...4] | |
| safeVal(value, $target) | |
| # Restrictions | |
| restrictNumeric = (e) -> | |
| # Key event is for a browser shortcut | |
| return true if e.metaKey or e.ctrlKey | |
| # If keycode is a space | |
| return false if e.which is 32 | |
| # If keycode is a special char (WebKit) | |
| return true if e.which is 0 | |
| # If char is a special char (Firefox) | |
| return true if e.which < 33 | |
| input = String.fromCharCode(e.which) | |
| # Char is a number or a space | |
| !!/[\d\s]/.test(input) | |
| restrictCardNumber = (e) -> | |
| $target = $(e.currentTarget) | |
| digit = String.fromCharCode(e.which) | |
| return unless /^\d+$/.test(digit) | |
| return if hasTextSelected($target) | |
| # Restrict number of digits | |
| value = ($target.val() + digit).replace(/\D/g, '') | |
| card = cardFromNumber(value) | |
| if card | |
| value.length <= card.length[card.length.length - 1] | |
| else | |
| # All other cards are 16 digits long | |
| value.length <= 16 | |
| restrictExpiry = (e) -> | |
| $target = $(e.currentTarget) | |
| digit = String.fromCharCode(e.which) | |
| return unless /^\d+$/.test(digit) | |
| return if hasTextSelected($target) | |
| value = $target.val() + digit | |
| value = value.replace(/\D/g, '') | |
| return false if value.length > 6 | |
| restrictCVC = (e) -> | |
| $target = $(e.currentTarget) | |
| digit = String.fromCharCode(e.which) | |
| return unless /^\d+$/.test(digit) | |
| return if hasTextSelected($target) | |
| val = $target.val() + digit | |
| val.length <= 4 | |
| setCardType = (e) -> | |
| $target = $(e.currentTarget) | |
| val = $target.val() | |
| cardType = $.payment.cardType(val) or 'unknown' | |
| unless $target.hasClass(cardType) | |
| allTypes = (card.type for card in cards) | |
| $target.removeClass('unknown') | |
| $target.removeClass(allTypes.join(' ')) | |
| $target.addClass(cardType) | |
| $target.toggleClass('identified', cardType isnt 'unknown') | |
| $target.trigger('payment.cardType', cardType) | |
| # Public | |
| # Formatting | |
| $.payment.fn.formatCardCVC = -> | |
| @on('keypress', restrictNumeric) | |
| @on('keypress', restrictCVC) | |
| @on('paste', reFormatCVC) | |
| @on('change', reFormatCVC) | |
| @on('input', reFormatCVC) | |
| this | |
| $.payment.fn.formatCardExpiry = -> | |
| @on('keypress', restrictNumeric) | |
| @on('keypress', restrictExpiry) | |
| @on('keypress', formatExpiry) | |
| @on('keypress', formatForwardSlashAndSpace) | |
| @on('keypress', formatForwardExpiry) | |
| @on('keydown', formatBackExpiry) | |
| @on('change', reFormatExpiry) | |
| @on('input', reFormatExpiry) | |
| this | |
| $.payment.fn.formatCardNumber = -> | |
| @on('keypress', restrictNumeric) | |
| @on('keypress', restrictCardNumber) | |
| @on('keypress', formatCardNumber) | |
| @on('keydown', formatBackCardNumber) | |
| @on('keyup', setCardType) | |
| @on('paste', reFormatCardNumber) | |
| @on('change', reFormatCardNumber) | |
| @on('input', reFormatCardNumber) | |
| @on('input', setCardType) | |
| this | |
| # Restrictions | |
| $.payment.fn.restrictNumeric = -> | |
| @on('keypress', restrictNumeric) | |
| @on('paste', reFormatNumeric) | |
| @on('change', reFormatNumeric) | |
| @on('input', reFormatNumeric) | |
| this | |
| # Validations | |
| $.payment.fn.cardExpiryVal = -> | |
| $.payment.cardExpiryVal($(this).val()) | |
| $.payment.cardExpiryVal = (value) -> | |
| [month, year] = value.split(/[\s\/]+/, 2) | |
| # Allow for year shortcut | |
| if year?.length is 2 and /^\d+$/.test(year) | |
| prefix = (new Date).getFullYear() | |
| prefix = prefix.toString()[0..1] | |
| year = prefix + year | |
| month = parseInt(month, 10) | |
| year = parseInt(year, 10) | |
| month: month, year: year | |
| $.payment.validateCardNumber = (num) -> | |
| num = (num + '').replace(/\s+|-/g, '') | |
| return false unless /^\d+$/.test(num) | |
| card = cardFromNumber(num) | |
| return false unless card | |
| num.length in card.length and | |
| (card.luhn is false or luhnCheck(num)) | |
| $.payment.validateCardExpiry = (month, year) -> | |
| # Allow passing an object | |
| if typeof month is 'object' and 'month' of month | |
| {month, year} = month | |
| return false unless month and year | |
| month = $.trim(month) | |
| year = $.trim(year) | |
| return false unless /^\d+$/.test(month) | |
| return false unless /^\d+$/.test(year) | |
| return false unless 1 <= month <= 12 | |
| if year.length == 2 | |
| if year < 70 | |
| year = "20#{year}" | |
| else | |
| year = "19#{year}" | |
| return false unless year.length == 4 | |
| expiry = new Date(year, month) | |
| currentTime = new Date | |
| # Months start from 0 in JavaScript | |
| expiry.setMonth(expiry.getMonth() - 1) | |
| # The cc expires at the end of the month, | |
| # so we need to make the expiry the first day | |
| # of the month after | |
| expiry.setMonth(expiry.getMonth() + 1, 1) | |
| expiry > currentTime | |
| $.payment.validateCardCVC = (cvc, type) -> | |
| cvc = $.trim(cvc) | |
| return false unless /^\d+$/.test(cvc) | |
| card = cardFromType(type) | |
| if card? | |
| # Check against a explicit card type | |
| cvc.length in card.cvcLength | |
| else | |
| # Check against all types | |
| cvc.length >= 3 and cvc.length <= 4 | |
| $.payment.cardType = (num) -> | |
| return null unless num | |
| cardFromNumber(num)?.type or null | |
| $.payment.formatCardNumber = (num) -> | |
| num = num.replace(/\D/g, '') | |
| card = cardFromNumber(num) | |
| return num unless card | |
| upperLength = card.length[card.length.length - 1] | |
| num = num[0...upperLength] | |
| if card.format.global | |
| num.match(card.format)?.join(' ') | |
| else | |
| groups = card.format.exec(num) | |
| return unless groups? | |
| groups.shift() | |
| groups = $.grep(groups, (n) -> n) # Filter empty groups | |
| groups.join(' ') | |
| $.payment.formatExpiry = (expiry) -> | |
| parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/) | |
| return '' unless parts | |
| mon = parts[1] || '' | |
| sep = parts[2] || '' | |
| year = parts[3] || '' | |
| if year.length > 0 | |
| sep = ' / ' | |
| else if sep is ' /' | |
| mon = mon.substring(0, 1) | |
| sep = '' | |
| else if mon.length == 2 or sep.length > 0 | |
| sep = ' / ' | |
| else if mon.length == 1 and mon not in ['0', '1'] | |
| mon = "0#{mon}" | |
| sep = ' / ' | |
| return mon + sep + year |