Просмотр исходного кода

Basic port of Stripe JS credit card validation and formatting lib.

Paulo Reis 10 лет назад
Родитель
Сommit
79d9e54458
1 измененных файлов с 395 добавлено и 0 удалено
  1. 395 0
      services/web/public/coffee/directives/creditCards.coffee

+ 395 - 0
services/web/public/coffee/directives/creditCards.coffee

@@ -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
+
+
+