|
@@ -0,0 +1,395 @@
|
|
|
|
|
+define [
|
|
|
|
|
+ "base"
|
|
|
|
|
+], (App) ->
|
|
|
|
|
+ App.factory 'ccUtils', () ->
|
|
|
|
|
+ defaultFormat = /(\d{1,4})/g;
|
|
|
|
|
+ defaultInputFormat = /(?:^|\s)(\d{4})$/
|
|
|
|
|
+
|
|
|
|
|
+ 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..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
|
|
|
|
|
+
|
|
|
|
|
+ parseExpiry = (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
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ fromNumber: cardFromNumber
|
|
|
|
|
+ fromType: cardFromType
|
|
|
|
|
+ defaultFormat: defaultFormat
|
|
|
|
|
+ defaultInputFormat: defaultInputFormat
|
|
|
|
|
+ parseExpiry: parseExpiry
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ App.factory 'ccFormat', (ccUtils, $filter) ->
|
|
|
|
|
+ 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
|
|
|
|
|
+
|
|
|
|
|
+ # 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 = ccUtils.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} / ")
|
|
|
|
|
+
|
|
|
|
|
+ formatForwardSlash = (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$/, ''))
|
|
|
|
|
+
|
|
|
|
|
+ parseExpiry = (value) ->
|
|
|
|
|
+ if value?
|
|
|
|
|
+ dateAsObj = ccUtils.parseExpiry(value);
|
|
|
|
|
+ expiry = new Date dateAsObj.year, dateAsObj.month - 1
|
|
|
|
|
+ return $filter('date')(expiry, 'MM/yyyy')
|
|
|
|
|
+
|
|
|
|
|
+ # 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)
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ hasTextSelected
|
|
|
|
|
+ replaceFullWidthChars
|
|
|
|
|
+ reFormatNumeric
|
|
|
|
|
+ reFormatCardNumber
|
|
|
|
|
+ formatCardNumber
|
|
|
|
|
+ formatBackCardNumber
|
|
|
|
|
+ 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.restrictExpiry
|
|
|
|
|
+ el.on 'keypress', ccFormat.formatExpiry
|
|
|
|
|
+ el.on 'keypress', ccFormat.formatForwardSlash
|
|
|
|
|
+ el.on 'keypress', ccFormat.formatForwardExpiry
|
|
|
|
|
+ el.on 'keydown', ccFormat.formatBackExpiry
|
|
|
|
|
+
|
|
|
|
|
+ ngModel.$parsers.push ccFormat.parseExpiry
|
|
|
|
|
+ ngModel.$formatters.push ccFormat.parseExpiry
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|