├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── Cakefile ├── LICENSE ├── README.md ├── bower.json ├── example └── index.html ├── lib ├── jquery.payment.js └── jquery.payment.min.js ├── package.json ├── payment.jquery.json ├── src └── jquery.payment.coffee └── test ├── jquery.coffee ├── specs.coffee └── zepto.coffee /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thanks for your interest in contributing! jquery.payment is deprecated. Please see "Project Status" in the README. 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | {spawn} = require 'child_process' 2 | path = require 'path' 3 | 4 | binPath = (bin) -> path.resolve(__dirname, "./node_modules/.bin/#{bin}") 5 | 6 | runExternal = (cmd, args, callback = process.exit) -> 7 | child = spawn(binPath(cmd), args, stdio: 'inherit') 8 | child.on('error', console.error) 9 | child.on('close', callback) 10 | 11 | runSequential = (cmds, status = 0) -> 12 | process.exit status if status or !cmds.length 13 | cmd = cmds.shift() 14 | cmd.push (status) -> runSequential cmds, status 15 | runExternal.apply null, cmd 16 | 17 | task 'build', 'Build lib/ from src/', -> 18 | runExternal 'coffee', 19 | ['-c', '-o', 'lib', 'src'], 20 | -> invoke 'minify' 21 | 22 | task 'minify', 'Minify lib/', -> 23 | runExternal 'uglifyjs', [ 24 | 'lib/jquery.payment.js', 25 | '--mangle', 26 | '--compress', 27 | '--output', 28 | 'lib/jquery.payment.min.js' 29 | ] 30 | 31 | task 'watch', 'Watch src/ for changes', -> 32 | runExternal 'coffee', ['-w', '-c', '-o', 'lib', 'src'] 33 | 34 | task 'test', 'Run tests', -> 35 | runSequential [ 36 | ['mocha', ['--compilers', 'coffee:coffee-script/register', 'test/jquery.coffee']] 37 | ['mocha', ['--compilers', 'coffee:coffee-script/register', 'test/zepto.coffee']] 38 | ] 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Stripe 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jQuery.payment [![Build Status](https://travis-ci.org/stripe/jquery.payment.svg?branch=master)](https://travis-ci.org/stripe/jquery.payment) 2 | 3 | A general purpose library for building credit card forms, validating inputs and formatting numbers. 4 | 5 | ## Project status 6 | 7 | **jquery.payment is deprecated. We recommend that you use either [Stripe Checkout](https://stripe.com/docs/checkout) or [Stripe Elements](https://stripe.com/docs/elements) to collect card information.** 8 | 9 | We will patch jquery.payment for major critical/security issues, but we won't be adding new features. 10 | 11 | ## Usage 12 | 13 | You can make an input act like a credit card field (with number formatting and length restriction): 14 | 15 | ``` javascript 16 | $('input.cc-num').payment('formatCardNumber'); 17 | ``` 18 | 19 | Then, when the payment form is submitted, you can validate the card number on the client-side: 20 | 21 | ``` javascript 22 | var valid = $.payment.validateCardNumber($('input.cc-num').val()); 23 | 24 | if (!valid) { 25 | alert('Your card is not valid!'); 26 | return false; 27 | } 28 | ``` 29 | 30 | You can find a full [demo here](http://stripe.github.io/jquery.payment/example). 31 | 32 | Supported card types are: 33 | 34 | * Visa 35 | * MasterCard 36 | * American Express 37 | * Diners Club 38 | * Discover 39 | * UnionPay 40 | * JCB 41 | * Maestro 42 | * Forbrugsforeningen 43 | * Dankort 44 | 45 | (Additional card types are supported by extending the [`$.payment.cards`](#paymentcards) array.) 46 | 47 | ## API 48 | 49 | ### $.fn.payment('formatCardNumber') 50 | 51 | Formats card numbers: 52 | 53 | * Includes a space between every 4 digits 54 | * Restricts input to numbers 55 | * Limits to 16 numbers 56 | * Supports American Express formatting 57 | * Adds a class of the card type (e.g. 'visa') to the input 58 | 59 | Example: 60 | 61 | ``` javascript 62 | $('input.cc-num').payment('formatCardNumber'); 63 | ``` 64 | 65 | ### $.fn.payment('formatCardExpiry') 66 | 67 | Formats card expiry: 68 | 69 | * Includes a `/` between the month and year 70 | * Restricts input to numbers 71 | * Restricts length 72 | 73 | Example: 74 | 75 | ``` javascript 76 | $('input.cc-exp').payment('formatCardExpiry'); 77 | ``` 78 | 79 | ### $.fn.payment('formatCardCVC') 80 | 81 | Formats card CVC: 82 | 83 | * Restricts length to 4 numbers 84 | * Restricts input to numbers 85 | 86 | Example: 87 | 88 | ``` javascript 89 | $('input.cc-cvc').payment('formatCardCVC'); 90 | ``` 91 | 92 | ### $.fn.payment('restrictNumeric') 93 | 94 | General numeric input restriction. 95 | 96 | Example: 97 | 98 | ``` javascript 99 | $('[data-numeric]').payment('restrictNumeric'); 100 | ``` 101 | 102 | ### $.payment.validateCardNumber(number) 103 | 104 | Validates a card number: 105 | 106 | * Validates numbers 107 | * Validates Luhn algorithm 108 | * Validates length 109 | 110 | Example: 111 | 112 | ``` javascript 113 | $.payment.validateCardNumber('4242 4242 4242 4242'); //=> true 114 | ``` 115 | 116 | ### $.payment.validateCardExpiry(month, year) 117 | 118 | Validates a card expiry: 119 | 120 | * Validates numbers 121 | * Validates in the future 122 | * Supports year shorthand 123 | 124 | Example: 125 | 126 | ``` javascript 127 | $.payment.validateCardExpiry('05', '20'); //=> true 128 | $.payment.validateCardExpiry('05', '2015'); //=> true 129 | $.payment.validateCardExpiry('05', '05'); //=> false 130 | ``` 131 | 132 | ### $.payment.validateCardCVC(cvc, type) 133 | 134 | Validates a card CVC: 135 | 136 | * Validates number 137 | * Validates length to 4 138 | 139 | Example: 140 | 141 | ``` javascript 142 | $.payment.validateCardCVC('123'); //=> true 143 | $.payment.validateCardCVC('123', 'amex'); //=> true 144 | $.payment.validateCardCVC('1234', 'amex'); //=> true 145 | $.payment.validateCardCVC('12344'); //=> false 146 | ``` 147 | 148 | ### $.payment.cardType(number) 149 | 150 | Returns a card type. Either: 151 | 152 | * `visa` 153 | * `mastercard` 154 | * `amex` 155 | * `dinersclub` 156 | * `discover` 157 | * `unionpay` 158 | * `jcb` 159 | * `maestro` 160 | * `forbrugsforeningen` 161 | * `dankort` 162 | 163 | The function will return `null` if the card type can't be determined. 164 | 165 | Example: 166 | 167 | ``` javascript 168 | $.payment.cardType('4242 4242 4242 4242'); //=> 'visa' 169 | ``` 170 | 171 | ### $.payment.cardExpiryVal(string) and $.fn.payment('cardExpiryVal') 172 | 173 | Parses a credit card expiry in the form of MM/YYYY, returning an object containing the `month` and `year`. Shorthand years, such as `13` are also supported (and converted into the longhand, e.g. `2013`). 174 | 175 | ``` javascript 176 | $.payment.cardExpiryVal('03 / 2025'); //=> {month: 3, year: 2025} 177 | $.payment.cardExpiryVal('05 / 04'); //=> {month: 5, year: 2004} 178 | $('input.cc-exp').payment('cardExpiryVal') //=> {month: 4, year: 2020} 179 | ``` 180 | 181 | This function doesn't perform any validation of the month or year; use `$.payment.validateCardExpiry(month, year)` for that. 182 | 183 | ### $.payment.cards 184 | 185 | Array of objects that describe valid card types. Each object should contain the following fields: 186 | 187 | ``` javascript 188 | { 189 | // Card type, as returned by $.payment.cardType. 190 | type: 'mastercard', 191 | // Array of prefixes used to identify the card type. 192 | patterns: [ 193 | 51, 52, 53, 54, 55, 194 | 22, 23, 24, 25, 26, 27 195 | ], 196 | // Array of valid card number lengths. 197 | length: [16], 198 | // Array of valid card CVC lengths. 199 | cvcLength: [3], 200 | // Boolean indicating whether a valid card number should satisfy the Luhn check. 201 | luhn: true, 202 | // Regex used to format the card number. Each match is joined with a space. 203 | format: /(\d{1,4})/g 204 | } 205 | ``` 206 | 207 | When identifying a card type, the array is traversed in order until the card number matches a prefix in `patterns`. For this reason, patterns with higher specificity should appear towards the beginning of the array. 208 | 209 | ## Example 210 | 211 | Look in [`./example/index.html`](example/index.html) 212 | 213 | ## Building 214 | 215 | Run `cake build` 216 | 217 | ## Running tests 218 | 219 | Run `cake test` 220 | 221 | ## Autocomplete recommendations 222 | 223 | We recommend you turn autocomplete on for credit card forms, *except for the CVC field (which should never be stored)*. You can do this by setting the `autocomplete` attribute: 224 | 225 | ``` html 226 |
227 | 228 | 229 | 230 |
231 | ``` 232 | 233 | You should mark up your fields using the [Autofill spec](https://html.spec.whatwg.org/multipage/forms.html#autofill). These are respected by a number of browsers, including Chrome, Safari, Firefox. 234 | 235 | ## Mobile recommendations 236 | 237 | We recommend you to use `` which will cause the numeric keyboard to be displayed on mobile devices: 238 | 239 | ``` html 240 | 241 | ``` 242 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery.payment", 3 | "main": "lib/jquery.payment.js", 4 | "dependencies": { 5 | "jquery": ">=1.5" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 22 | 23 | 50 | 51 | 52 | 53 |
54 |

55 | jquery.payment demo 56 | Fork on GitHub 57 |

58 |

A general purpose library for building credit card forms, validating inputs and formatting numbers.

59 |
60 |
61 | 62 | 63 |
64 | 65 |
66 | 67 | 68 |
69 | 70 |
71 | 72 | 73 |
74 | 75 |
76 | 77 | 78 |
79 | 80 | 81 | 82 |

83 |
84 |
85 | 86 | 87 | -------------------------------------------------------------------------------- /lib/jquery.payment.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.7.1 2 | (function() { 3 | var $, cardFromNumber, cardFromType, cards, defaultFormat, formatBackCardNumber, formatBackExpiry, formatCardNumber, formatExpiry, formatForwardExpiry, formatForwardSlashAndSpace, hasTextSelected, luhnCheck, reFormatCVC, reFormatCardNumber, reFormatExpiry, reFormatNumeric, replaceFullWidthChars, restrictCVC, restrictCardNumber, restrictExpiry, restrictNumeric, safeVal, setCardType, 4 | __slice = [].slice, 5 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 6 | 7 | $ = window.jQuery || window.Zepto || window.$; 8 | 9 | $.payment = {}; 10 | 11 | $.payment.fn = {}; 12 | 13 | $.fn.payment = function() { 14 | var args, method; 15 | method = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 16 | return $.payment.fn[method].apply(this, args); 17 | }; 18 | 19 | defaultFormat = /(\d{1,4})/g; 20 | 21 | $.payment.cards = cards = [ 22 | { 23 | type: 'maestro', 24 | patterns: [5018, 502, 503, 506, 56, 58, 639, 6220, 67], 25 | format: defaultFormat, 26 | length: [12, 13, 14, 15, 16, 17, 18, 19], 27 | cvcLength: [3], 28 | luhn: true 29 | }, { 30 | type: 'forbrugsforeningen', 31 | patterns: [600], 32 | format: defaultFormat, 33 | length: [16], 34 | cvcLength: [3], 35 | luhn: true 36 | }, { 37 | type: 'dankort', 38 | patterns: [5019], 39 | format: defaultFormat, 40 | length: [16], 41 | cvcLength: [3], 42 | luhn: true 43 | }, { 44 | type: 'visa', 45 | patterns: [4], 46 | format: defaultFormat, 47 | length: [13, 16], 48 | cvcLength: [3], 49 | luhn: true 50 | }, { 51 | type: 'mastercard', 52 | patterns: [51, 52, 53, 54, 55, 22, 23, 24, 25, 26, 27], 53 | format: defaultFormat, 54 | length: [16], 55 | cvcLength: [3], 56 | luhn: true 57 | }, { 58 | type: 'amex', 59 | patterns: [34, 37], 60 | format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/, 61 | length: [15], 62 | cvcLength: [3, 4], 63 | luhn: true 64 | }, { 65 | type: 'dinersclub', 66 | patterns: [30, 36, 38, 39], 67 | format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/, 68 | length: [14], 69 | cvcLength: [3], 70 | luhn: true 71 | }, { 72 | type: 'discover', 73 | patterns: [60, 64, 65, 622], 74 | format: defaultFormat, 75 | length: [16], 76 | cvcLength: [3], 77 | luhn: true 78 | }, { 79 | type: 'unionpay', 80 | patterns: [62, 88], 81 | format: defaultFormat, 82 | length: [16, 17, 18, 19], 83 | cvcLength: [3], 84 | luhn: false 85 | }, { 86 | type: 'jcb', 87 | patterns: [35], 88 | format: defaultFormat, 89 | length: [16], 90 | cvcLength: [3], 91 | luhn: true 92 | } 93 | ]; 94 | 95 | cardFromNumber = function(num) { 96 | var card, p, pattern, _i, _j, _len, _len1, _ref; 97 | num = (num + '').replace(/\D/g, ''); 98 | for (_i = 0, _len = cards.length; _i < _len; _i++) { 99 | card = cards[_i]; 100 | _ref = card.patterns; 101 | for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { 102 | pattern = _ref[_j]; 103 | p = pattern + ''; 104 | if (num.substr(0, p.length) === p) { 105 | return card; 106 | } 107 | } 108 | } 109 | }; 110 | 111 | cardFromType = function(type) { 112 | var card, _i, _len; 113 | for (_i = 0, _len = cards.length; _i < _len; _i++) { 114 | card = cards[_i]; 115 | if (card.type === type) { 116 | return card; 117 | } 118 | } 119 | }; 120 | 121 | luhnCheck = function(num) { 122 | var digit, digits, odd, sum, _i, _len; 123 | odd = true; 124 | sum = 0; 125 | digits = (num + '').split('').reverse(); 126 | for (_i = 0, _len = digits.length; _i < _len; _i++) { 127 | digit = digits[_i]; 128 | digit = parseInt(digit, 10); 129 | if ((odd = !odd)) { 130 | digit *= 2; 131 | } 132 | if (digit > 9) { 133 | digit -= 9; 134 | } 135 | sum += digit; 136 | } 137 | return sum % 10 === 0; 138 | }; 139 | 140 | hasTextSelected = function($target) { 141 | var _ref; 142 | if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== $target.prop('selectionEnd')) { 143 | return true; 144 | } 145 | if ((typeof document !== "undefined" && document !== null ? (_ref = document.selection) != null ? _ref.createRange : void 0 : void 0) != null) { 146 | if (document.selection.createRange().text) { 147 | return true; 148 | } 149 | } 150 | return false; 151 | }; 152 | 153 | safeVal = function(value, $target) { 154 | var currPair, cursor, digit, error, last, prevPair; 155 | try { 156 | cursor = $target.prop('selectionStart'); 157 | } catch (_error) { 158 | error = _error; 159 | cursor = null; 160 | } 161 | last = $target.val(); 162 | $target.val(value); 163 | if (cursor !== null && $target.is(":focus")) { 164 | if (cursor === last.length) { 165 | cursor = value.length; 166 | } 167 | if (last !== value) { 168 | prevPair = last.slice(cursor - 1, +cursor + 1 || 9e9); 169 | currPair = value.slice(cursor - 1, +cursor + 1 || 9e9); 170 | digit = value[cursor]; 171 | if (/\d/.test(digit) && prevPair === ("" + digit + " ") && currPair === (" " + digit)) { 172 | cursor = cursor + 1; 173 | } 174 | } 175 | $target.prop('selectionStart', cursor); 176 | return $target.prop('selectionEnd', cursor); 177 | } 178 | }; 179 | 180 | replaceFullWidthChars = function(str) { 181 | var chars, chr, fullWidth, halfWidth, idx, value, _i, _len; 182 | if (str == null) { 183 | str = ''; 184 | } 185 | fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19'; 186 | halfWidth = '0123456789'; 187 | value = ''; 188 | chars = str.split(''); 189 | for (_i = 0, _len = chars.length; _i < _len; _i++) { 190 | chr = chars[_i]; 191 | idx = fullWidth.indexOf(chr); 192 | if (idx > -1) { 193 | chr = halfWidth[idx]; 194 | } 195 | value += chr; 196 | } 197 | return value; 198 | }; 199 | 200 | reFormatNumeric = function(e) { 201 | var $target; 202 | $target = $(e.currentTarget); 203 | return setTimeout(function() { 204 | var value; 205 | value = $target.val(); 206 | value = replaceFullWidthChars(value); 207 | value = value.replace(/\D/g, ''); 208 | return safeVal(value, $target); 209 | }); 210 | }; 211 | 212 | reFormatCardNumber = function(e) { 213 | var $target; 214 | $target = $(e.currentTarget); 215 | return setTimeout(function() { 216 | var value; 217 | value = $target.val(); 218 | value = replaceFullWidthChars(value); 219 | value = $.payment.formatCardNumber(value); 220 | return safeVal(value, $target); 221 | }); 222 | }; 223 | 224 | formatCardNumber = function(e) { 225 | var $target, card, digit, length, re, upperLength, value; 226 | digit = String.fromCharCode(e.which); 227 | if (!/^\d+$/.test(digit)) { 228 | return; 229 | } 230 | $target = $(e.currentTarget); 231 | value = $target.val(); 232 | card = cardFromNumber(value + digit); 233 | length = (value.replace(/\D/g, '') + digit).length; 234 | upperLength = 16; 235 | if (card) { 236 | upperLength = card.length[card.length.length - 1]; 237 | } 238 | if (length >= upperLength) { 239 | return; 240 | } 241 | if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) { 242 | return; 243 | } 244 | if (card && card.type === 'amex') { 245 | re = /^(\d{4}|\d{4}\s\d{6})$/; 246 | } else { 247 | re = /(?:^|\s)(\d{4})$/; 248 | } 249 | if (re.test(value)) { 250 | e.preventDefault(); 251 | return setTimeout(function() { 252 | return $target.val(value + ' ' + digit); 253 | }); 254 | } else if (re.test(value + digit)) { 255 | e.preventDefault(); 256 | return setTimeout(function() { 257 | return $target.val(value + digit + ' '); 258 | }); 259 | } 260 | }; 261 | 262 | formatBackCardNumber = function(e) { 263 | var $target, value; 264 | $target = $(e.currentTarget); 265 | value = $target.val(); 266 | if (e.which !== 8) { 267 | return; 268 | } 269 | if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) { 270 | return; 271 | } 272 | if (/\d\s$/.test(value)) { 273 | e.preventDefault(); 274 | return setTimeout(function() { 275 | return $target.val(value.replace(/\d\s$/, '')); 276 | }); 277 | } else if (/\s\d?$/.test(value)) { 278 | e.preventDefault(); 279 | return setTimeout(function() { 280 | return $target.val(value.replace(/\d$/, '')); 281 | }); 282 | } 283 | }; 284 | 285 | reFormatExpiry = function(e) { 286 | var $target; 287 | $target = $(e.currentTarget); 288 | return setTimeout(function() { 289 | var value; 290 | value = $target.val(); 291 | value = replaceFullWidthChars(value); 292 | value = $.payment.formatExpiry(value); 293 | return safeVal(value, $target); 294 | }); 295 | }; 296 | 297 | formatExpiry = function(e) { 298 | var $target, digit, val; 299 | digit = String.fromCharCode(e.which); 300 | if (!/^\d+$/.test(digit)) { 301 | return; 302 | } 303 | $target = $(e.currentTarget); 304 | val = $target.val() + digit; 305 | if (/^\d$/.test(val) && (val !== '0' && val !== '1')) { 306 | e.preventDefault(); 307 | return setTimeout(function() { 308 | return $target.val("0" + val + " / "); 309 | }); 310 | } else if (/^\d\d$/.test(val)) { 311 | e.preventDefault(); 312 | return setTimeout(function() { 313 | var m1, m2; 314 | m1 = parseInt(val[0], 10); 315 | m2 = parseInt(val[1], 10); 316 | if (m2 > 2 && m1 !== 0) { 317 | return $target.val("0" + m1 + " / " + m2); 318 | } else { 319 | return $target.val("" + val + " / "); 320 | } 321 | }); 322 | } 323 | }; 324 | 325 | formatForwardExpiry = function(e) { 326 | var $target, digit, val; 327 | digit = String.fromCharCode(e.which); 328 | if (!/^\d+$/.test(digit)) { 329 | return; 330 | } 331 | $target = $(e.currentTarget); 332 | val = $target.val(); 333 | if (/^\d\d$/.test(val)) { 334 | return $target.val("" + val + " / "); 335 | } 336 | }; 337 | 338 | formatForwardSlashAndSpace = function(e) { 339 | var $target, val, which; 340 | which = String.fromCharCode(e.which); 341 | if (!(which === '/' || which === ' ')) { 342 | return; 343 | } 344 | $target = $(e.currentTarget); 345 | val = $target.val(); 346 | if (/^\d$/.test(val) && val !== '0') { 347 | return $target.val("0" + val + " / "); 348 | } 349 | }; 350 | 351 | formatBackExpiry = function(e) { 352 | var $target, value; 353 | $target = $(e.currentTarget); 354 | value = $target.val(); 355 | if (e.which !== 8) { 356 | return; 357 | } 358 | if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) { 359 | return; 360 | } 361 | if (/\d\s\/\s$/.test(value)) { 362 | e.preventDefault(); 363 | return setTimeout(function() { 364 | return $target.val(value.replace(/\d\s\/\s$/, '')); 365 | }); 366 | } 367 | }; 368 | 369 | reFormatCVC = function(e) { 370 | var $target; 371 | $target = $(e.currentTarget); 372 | return setTimeout(function() { 373 | var value; 374 | value = $target.val(); 375 | value = replaceFullWidthChars(value); 376 | value = value.replace(/\D/g, '').slice(0, 4); 377 | return safeVal(value, $target); 378 | }); 379 | }; 380 | 381 | restrictNumeric = function(e) { 382 | var input; 383 | if (e.metaKey || e.ctrlKey) { 384 | return true; 385 | } 386 | if (e.which === 32) { 387 | return false; 388 | } 389 | if (e.which === 0) { 390 | return true; 391 | } 392 | if (e.which < 33) { 393 | return true; 394 | } 395 | input = String.fromCharCode(e.which); 396 | return !!/[\d\s]/.test(input); 397 | }; 398 | 399 | restrictCardNumber = function(e) { 400 | var $target, card, digit, value; 401 | $target = $(e.currentTarget); 402 | digit = String.fromCharCode(e.which); 403 | if (!/^\d+$/.test(digit)) { 404 | return; 405 | } 406 | if (hasTextSelected($target)) { 407 | return; 408 | } 409 | value = ($target.val() + digit).replace(/\D/g, ''); 410 | card = cardFromNumber(value); 411 | if (card) { 412 | return value.length <= card.length[card.length.length - 1]; 413 | } else { 414 | return value.length <= 16; 415 | } 416 | }; 417 | 418 | restrictExpiry = function(e) { 419 | var $target, digit, value; 420 | $target = $(e.currentTarget); 421 | digit = String.fromCharCode(e.which); 422 | if (!/^\d+$/.test(digit)) { 423 | return; 424 | } 425 | if (hasTextSelected($target)) { 426 | return; 427 | } 428 | value = $target.val() + digit; 429 | value = value.replace(/\D/g, ''); 430 | if (value.length > 6) { 431 | return false; 432 | } 433 | }; 434 | 435 | restrictCVC = function(e) { 436 | var $target, digit, val; 437 | $target = $(e.currentTarget); 438 | digit = String.fromCharCode(e.which); 439 | if (!/^\d+$/.test(digit)) { 440 | return; 441 | } 442 | if (hasTextSelected($target)) { 443 | return; 444 | } 445 | val = $target.val() + digit; 446 | return val.length <= 4; 447 | }; 448 | 449 | setCardType = function(e) { 450 | var $target, allTypes, card, cardType, val; 451 | $target = $(e.currentTarget); 452 | val = $target.val(); 453 | cardType = $.payment.cardType(val) || 'unknown'; 454 | if (!$target.hasClass(cardType)) { 455 | allTypes = (function() { 456 | var _i, _len, _results; 457 | _results = []; 458 | for (_i = 0, _len = cards.length; _i < _len; _i++) { 459 | card = cards[_i]; 460 | _results.push(card.type); 461 | } 462 | return _results; 463 | })(); 464 | $target.removeClass('unknown'); 465 | $target.removeClass(allTypes.join(' ')); 466 | $target.addClass(cardType); 467 | $target.toggleClass('identified', cardType !== 'unknown'); 468 | return $target.trigger('payment.cardType', cardType); 469 | } 470 | }; 471 | 472 | $.payment.fn.formatCardCVC = function() { 473 | this.on('keypress', restrictNumeric); 474 | this.on('keypress', restrictCVC); 475 | this.on('paste', reFormatCVC); 476 | this.on('change', reFormatCVC); 477 | this.on('input', reFormatCVC); 478 | return this; 479 | }; 480 | 481 | $.payment.fn.formatCardExpiry = function() { 482 | this.on('keypress', restrictNumeric); 483 | this.on('keypress', restrictExpiry); 484 | this.on('keypress', formatExpiry); 485 | this.on('keypress', formatForwardSlashAndSpace); 486 | this.on('keypress', formatForwardExpiry); 487 | this.on('keydown', formatBackExpiry); 488 | this.on('change', reFormatExpiry); 489 | this.on('input', reFormatExpiry); 490 | return this; 491 | }; 492 | 493 | $.payment.fn.formatCardNumber = function() { 494 | this.on('keypress', restrictNumeric); 495 | this.on('keypress', restrictCardNumber); 496 | this.on('keypress', formatCardNumber); 497 | this.on('keydown', formatBackCardNumber); 498 | this.on('keyup', setCardType); 499 | this.on('paste', reFormatCardNumber); 500 | this.on('change', reFormatCardNumber); 501 | this.on('input', reFormatCardNumber); 502 | this.on('input', setCardType); 503 | return this; 504 | }; 505 | 506 | $.payment.fn.restrictNumeric = function() { 507 | this.on('keypress', restrictNumeric); 508 | this.on('paste', reFormatNumeric); 509 | this.on('change', reFormatNumeric); 510 | this.on('input', reFormatNumeric); 511 | return this; 512 | }; 513 | 514 | $.payment.fn.cardExpiryVal = function() { 515 | return $.payment.cardExpiryVal($(this).val()); 516 | }; 517 | 518 | $.payment.cardExpiryVal = function(value) { 519 | var month, prefix, year, _ref; 520 | _ref = value.split(/[\s\/]+/, 2), month = _ref[0], year = _ref[1]; 521 | if ((year != null ? year.length : void 0) === 2 && /^\d+$/.test(year)) { 522 | prefix = (new Date).getFullYear(); 523 | prefix = prefix.toString().slice(0, 2); 524 | year = prefix + year; 525 | } 526 | month = parseInt(month, 10); 527 | year = parseInt(year, 10); 528 | return { 529 | month: month, 530 | year: year 531 | }; 532 | }; 533 | 534 | $.payment.validateCardNumber = function(num) { 535 | var card, _ref; 536 | num = (num + '').replace(/\s+|-/g, ''); 537 | if (!/^\d+$/.test(num)) { 538 | return false; 539 | } 540 | card = cardFromNumber(num); 541 | if (!card) { 542 | return false; 543 | } 544 | return (_ref = num.length, __indexOf.call(card.length, _ref) >= 0) && (card.luhn === false || luhnCheck(num)); 545 | }; 546 | 547 | $.payment.validateCardExpiry = function(month, year) { 548 | var currentTime, expiry, _ref; 549 | if (typeof month === 'object' && 'month' in month) { 550 | _ref = month, month = _ref.month, year = _ref.year; 551 | } 552 | if (!(month && year)) { 553 | return false; 554 | } 555 | month = $.trim(month); 556 | year = $.trim(year); 557 | if (!/^\d+$/.test(month)) { 558 | return false; 559 | } 560 | if (!/^\d+$/.test(year)) { 561 | return false; 562 | } 563 | if (!((1 <= month && month <= 12))) { 564 | return false; 565 | } 566 | if (year.length === 2) { 567 | if (year < 70) { 568 | year = "20" + year; 569 | } else { 570 | year = "19" + year; 571 | } 572 | } 573 | if (year.length !== 4) { 574 | return false; 575 | } 576 | expiry = new Date(year, month); 577 | currentTime = new Date; 578 | expiry.setMonth(expiry.getMonth() - 1); 579 | expiry.setMonth(expiry.getMonth() + 1, 1); 580 | return expiry > currentTime; 581 | }; 582 | 583 | $.payment.validateCardCVC = function(cvc, type) { 584 | var card, _ref; 585 | cvc = $.trim(cvc); 586 | if (!/^\d+$/.test(cvc)) { 587 | return false; 588 | } 589 | card = cardFromType(type); 590 | if (card != null) { 591 | return _ref = cvc.length, __indexOf.call(card.cvcLength, _ref) >= 0; 592 | } else { 593 | return cvc.length >= 3 && cvc.length <= 4; 594 | } 595 | }; 596 | 597 | $.payment.cardType = function(num) { 598 | var _ref; 599 | if (!num) { 600 | return null; 601 | } 602 | return ((_ref = cardFromNumber(num)) != null ? _ref.type : void 0) || null; 603 | }; 604 | 605 | $.payment.formatCardNumber = function(num) { 606 | var card, groups, upperLength, _ref; 607 | num = num.replace(/\D/g, ''); 608 | card = cardFromNumber(num); 609 | if (!card) { 610 | return num; 611 | } 612 | upperLength = card.length[card.length.length - 1]; 613 | num = num.slice(0, upperLength); 614 | if (card.format.global) { 615 | return (_ref = num.match(card.format)) != null ? _ref.join(' ') : void 0; 616 | } else { 617 | groups = card.format.exec(num); 618 | if (groups == null) { 619 | return; 620 | } 621 | groups.shift(); 622 | groups = $.grep(groups, function(n) { 623 | return n; 624 | }); 625 | return groups.join(' '); 626 | } 627 | }; 628 | 629 | $.payment.formatExpiry = function(expiry) { 630 | var mon, parts, sep, year; 631 | parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/); 632 | if (!parts) { 633 | return ''; 634 | } 635 | mon = parts[1] || ''; 636 | sep = parts[2] || ''; 637 | year = parts[3] || ''; 638 | if (year.length > 0) { 639 | sep = ' / '; 640 | } else if (sep === ' /') { 641 | mon = mon.substring(0, 1); 642 | sep = ''; 643 | } else if (mon.length === 2 || sep.length > 0) { 644 | sep = ' / '; 645 | } else if (mon.length === 1 && (mon !== '0' && mon !== '1')) { 646 | mon = "0" + mon; 647 | sep = ' / '; 648 | } 649 | return mon + sep + year; 650 | }; 651 | 652 | }).call(this); 653 | -------------------------------------------------------------------------------- /lib/jquery.payment.min.js: -------------------------------------------------------------------------------- 1 | (function(){var t,e,n,r,a,o,i,l,u,s,c,h,p,g,v,f,d,m,y,C,T,w,$,D,S=[].slice,k=[].indexOf||function(t){for(var e=0,n=this.length;n>e;e++)if(e in this&&this[e]===t)return e;return-1};t=window.jQuery||window.Zepto||window.$,t.payment={},t.payment.fn={},t.fn.payment=function(){var e,n;return n=arguments[0],e=2<=arguments.length?S.call(arguments,1):[],t.payment.fn[n].apply(this,e)},a=/(\d{1,4})/g,t.payment.cards=r=[{type:"maestro",patterns:[5018,502,503,506,56,58,639,6220,67],format:a,length:[12,13,14,15,16,17,18,19],cvcLength:[3],luhn:!0},{type:"forbrugsforeningen",patterns:[600],format:a,length:[16],cvcLength:[3],luhn:!0},{type:"dankort",patterns:[5019],format:a,length:[16],cvcLength:[3],luhn:!0},{type:"visa",patterns:[4],format:a,length:[13,16],cvcLength:[3],luhn:!0},{type:"mastercard",patterns:[51,52,53,54,55,22,23,24,25,26,27],format:a,length:[16],cvcLength:[3],luhn:!0},{type:"amex",patterns:[34,37],format:/(\d{1,4})(\d{1,6})?(\d{1,5})?/,length:[15],cvcLength:[3,4],luhn:!0},{type:"dinersclub",patterns:[30,36,38,39],format:/(\d{1,4})(\d{1,6})?(\d{1,4})?/,length:[14],cvcLength:[3],luhn:!0},{type:"discover",patterns:[60,64,65,622],format:a,length:[16],cvcLength:[3],luhn:!0},{type:"unionpay",patterns:[62,88],format:a,length:[16,17,18,19],cvcLength:[3],luhn:!1},{type:"jcb",patterns:[35],format:a,length:[16],cvcLength:[3],luhn:!0}],e=function(t){var e,n,a,o,i,l,u,s;for(t=(t+"").replace(/\D/g,""),o=0,l=r.length;l>o;o++)for(e=r[o],s=e.patterns,i=0,u=s.length;u>i;i++)if(a=s[i],n=a+"",t.substr(0,n.length)===n)return e},n=function(t){var e,n,a;for(n=0,a=r.length;a>n;n++)if(e=r[n],e.type===t)return e},p=function(t){var e,n,r,a,o,i;for(r=!0,a=0,n=(t+"").split("").reverse(),o=0,i=n.length;i>o;o++)e=n[o],e=parseInt(e,10),(r=!r)&&(e*=2),e>9&&(e-=9),a+=e;return a%10===0},h=function(t){var e;return null!=t.prop("selectionStart")&&t.prop("selectionStart")!==t.prop("selectionEnd")?!0:null!=("undefined"!=typeof document&&null!==document&&null!=(e=document.selection)?e.createRange:void 0)&&document.selection.createRange().text?!0:!1},$=function(t,e){var n,r,a,o,i,l;try{r=e.prop("selectionStart")}catch(u){o=u,r=null}return i=e.val(),e.val(t),null!==r&&e.is(":focus")?(r===i.length&&(r=t.length),i!==t&&(l=i.slice(r-1,+r+1||9e9),n=t.slice(r-1,+r+1||9e9),a=t[r],/\d/.test(a)&&l===""+a+" "&&n===" "+a&&(r+=1)),e.prop("selectionStart",r),e.prop("selectionEnd",r)):void 0},m=function(t){var e,n,r,a,o,i,l,u;for(null==t&&(t=""),r="0123456789",a="0123456789",i="",e=t.split(""),l=0,u=e.length;u>l;l++)n=e[l],o=r.indexOf(n),o>-1&&(n=a[o]),i+=n;return i},d=function(e){var n;return n=t(e.currentTarget),setTimeout(function(){var t;return t=n.val(),t=m(t),t=t.replace(/\D/g,""),$(t,n)})},v=function(e){var n;return n=t(e.currentTarget),setTimeout(function(){var e;return e=n.val(),e=m(e),e=t.payment.formatCardNumber(e),$(e,n)})},l=function(n){var r,a,o,i,l,u,s;return o=String.fromCharCode(n.which),!/^\d+$/.test(o)||(r=t(n.currentTarget),s=r.val(),a=e(s+o),i=(s.replace(/\D/g,"")+o).length,u=16,a&&(u=a.length[a.length.length-1]),i>=u||null!=r.prop("selectionStart")&&r.prop("selectionStart")!==s.length)?void 0:(l=a&&"amex"===a.type?/^(\d{4}|\d{4}\s\d{6})$/:/(?:^|\s)(\d{4})$/,l.test(s)?(n.preventDefault(),setTimeout(function(){return r.val(s+" "+o)})):l.test(s+o)?(n.preventDefault(),setTimeout(function(){return r.val(s+o+" ")})):void 0)},o=function(e){var n,r;return n=t(e.currentTarget),r=n.val(),8!==e.which||null!=n.prop("selectionStart")&&n.prop("selectionStart")!==r.length?void 0:/\d\s$/.test(r)?(e.preventDefault(),setTimeout(function(){return n.val(r.replace(/\d\s$/,""))})):/\s\d?$/.test(r)?(e.preventDefault(),setTimeout(function(){return n.val(r.replace(/\d$/,""))})):void 0},f=function(e){var n;return n=t(e.currentTarget),setTimeout(function(){var e;return e=n.val(),e=m(e),e=t.payment.formatExpiry(e),$(e,n)})},u=function(e){var n,r,a;return r=String.fromCharCode(e.which),/^\d+$/.test(r)?(n=t(e.currentTarget),a=n.val()+r,/^\d$/.test(a)&&"0"!==a&&"1"!==a?(e.preventDefault(),setTimeout(function(){return n.val("0"+a+" / ")})):/^\d\d$/.test(a)?(e.preventDefault(),setTimeout(function(){var t,e;return t=parseInt(a[0],10),e=parseInt(a[1],10),e>2&&0!==t?n.val("0"+t+" / "+e):n.val(""+a+" / ")})):void 0):void 0},s=function(e){var n,r,a;return r=String.fromCharCode(e.which),/^\d+$/.test(r)?(n=t(e.currentTarget),a=n.val(),/^\d\d$/.test(a)?n.val(""+a+" / "):void 0):void 0},c=function(e){var n,r,a;return a=String.fromCharCode(e.which),"/"===a||" "===a?(n=t(e.currentTarget),r=n.val(),/^\d$/.test(r)&&"0"!==r?n.val("0"+r+" / "):void 0):void 0},i=function(e){var n,r;return n=t(e.currentTarget),r=n.val(),8!==e.which||null!=n.prop("selectionStart")&&n.prop("selectionStart")!==r.length?void 0:/\d\s\/\s$/.test(r)?(e.preventDefault(),setTimeout(function(){return n.val(r.replace(/\d\s\/\s$/,""))})):void 0},g=function(e){var n;return n=t(e.currentTarget),setTimeout(function(){var t;return t=n.val(),t=m(t),t=t.replace(/\D/g,"").slice(0,4),$(t,n)})},w=function(t){var e;return t.metaKey||t.ctrlKey?!0:32===t.which?!1:0===t.which?!0:t.which<33?!0:(e=String.fromCharCode(t.which),!!/[\d\s]/.test(e))},C=function(n){var r,a,o,i;return r=t(n.currentTarget),o=String.fromCharCode(n.which),/^\d+$/.test(o)&&!h(r)?(i=(r.val()+o).replace(/\D/g,""),a=e(i),a?i.length<=a.length[a.length.length-1]:i.length<=16):void 0},T=function(e){var n,r,a;return n=t(e.currentTarget),r=String.fromCharCode(e.which),/^\d+$/.test(r)&&!h(n)?(a=n.val()+r,a=a.replace(/\D/g,""),a.length>6?!1:void 0):void 0},y=function(e){var n,r,a;return n=t(e.currentTarget),r=String.fromCharCode(e.which),/^\d+$/.test(r)&&!h(n)?(a=n.val()+r,a.length<=4):void 0},D=function(e){var n,a,o,i,l;return n=t(e.currentTarget),l=n.val(),i=t.payment.cardType(l)||"unknown",n.hasClass(i)?void 0:(a=function(){var t,e,n;for(n=[],t=0,e=r.length;e>t;t++)o=r[t],n.push(o.type);return n}(),n.removeClass("unknown"),n.removeClass(a.join(" ")),n.addClass(i),n.toggleClass("identified","unknown"!==i),n.trigger("payment.cardType",i))},t.payment.fn.formatCardCVC=function(){return this.on("keypress",w),this.on("keypress",y),this.on("paste",g),this.on("change",g),this.on("input",g),this},t.payment.fn.formatCardExpiry=function(){return this.on("keypress",w),this.on("keypress",T),this.on("keypress",u),this.on("keypress",c),this.on("keypress",s),this.on("keydown",i),this.on("change",f),this.on("input",f),this},t.payment.fn.formatCardNumber=function(){return this.on("keypress",w),this.on("keypress",C),this.on("keypress",l),this.on("keydown",o),this.on("keyup",D),this.on("paste",v),this.on("change",v),this.on("input",v),this.on("input",D),this},t.payment.fn.restrictNumeric=function(){return this.on("keypress",w),this.on("paste",d),this.on("change",d),this.on("input",d),this},t.payment.fn.cardExpiryVal=function(){return t.payment.cardExpiryVal(t(this).val())},t.payment.cardExpiryVal=function(t){var e,n,r,a;return a=t.split(/[\s\/]+/,2),e=a[0],r=a[1],2===(null!=r?r.length:void 0)&&/^\d+$/.test(r)&&(n=(new Date).getFullYear(),n=n.toString().slice(0,2),r=n+r),e=parseInt(e,10),r=parseInt(r,10),{month:e,year:r}},t.payment.validateCardNumber=function(t){var n,r;return t=(t+"").replace(/\s+|-/g,""),/^\d+$/.test(t)?(n=e(t),n?(r=t.length,k.call(n.length,r)>=0&&(n.luhn===!1||p(t))):!1):!1},t.payment.validateCardExpiry=function(e,n){var r,a,o;return"object"==typeof e&&"month"in e&&(o=e,e=o.month,n=o.year),e&&n?(e=t.trim(e),n=t.trim(n),/^\d+$/.test(e)&&/^\d+$/.test(n)&&e>=1&&12>=e?(2===n.length&&(n=70>n?"20"+n:"19"+n),4!==n.length?!1:(a=new Date(n,e),r=new Date,a.setMonth(a.getMonth()-1),a.setMonth(a.getMonth()+1,1),a>r)):!1):!1},t.payment.validateCardCVC=function(e,r){var a,o;return e=t.trim(e),/^\d+$/.test(e)?(a=n(r),null!=a?(o=e.length,k.call(a.cvcLength,o)>=0):e.length>=3&&e.length<=4):!1},t.payment.cardType=function(t){var n;return t?(null!=(n=e(t))?n.type:void 0)||null:null},t.payment.formatCardNumber=function(n){var r,a,o,i;return n=n.replace(/\D/g,""),(r=e(n))?(o=r.length[r.length.length-1],n=n.slice(0,o),r.format.global?null!=(i=n.match(r.format))?i.join(" "):void 0:(a=r.format.exec(n),null!=a?(a.shift(),a=t.grep(a,function(t){return t}),a.join(" ")):void 0)):n},t.payment.formatExpiry=function(t){var e,n,r,a;return(n=t.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/))?(e=n[1]||"",r=n[2]||"",a=n[3]||"",a.length>0?r=" / ":" /"===r?(e=e.substring(0,1),r=""):2===e.length||r.length>0?r=" / ":1===e.length&&"0"!==e&&"1"!==e&&(e="0"+e,r=" / "),e+r+a):""}}).call(this); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery.payment", 3 | "version": "3.0.0", 4 | "description": "A general purpose library for building credit card forms, validating inputs and formatting numbers.", 5 | "keywords": [ 6 | "payment", 7 | "cc", 8 | "card" 9 | ], 10 | "author": "Stripe (https://www.stripe.com)", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/stripe/jquery.payment.git" 15 | }, 16 | "main": "lib/jquery.payment.js", 17 | "scripts": { 18 | "test": "cake test" 19 | }, 20 | "dependencies": { 21 | "jquery": ">=1.7" 22 | }, 23 | "devDependencies": { 24 | "cake": "~0.1", 25 | "coffee-script": "~1.7", 26 | "jsdom": "~7.2", 27 | "mocha": "~1.18", 28 | "uglify-js": "~2.4.24", 29 | "zeptojs": "~1.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /payment.jquery.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payment", 3 | "version": "3.0.0", 4 | "title": "jQuery.payment", 5 | "description": "A general purpose library for building credit card forms, validating inputs and formatting numbers.", 6 | "keywords": [ 7 | "payment", 8 | "cc", 9 | "card" 10 | ], 11 | "author": { 12 | "name": "Stripe", 13 | "url": "https://www.stripe.com", 14 | "email": "support+github@stripe.com" 15 | }, 16 | "licenses": [ 17 | { 18 | "type": "MIT", 19 | "url": "https://github.com/stripe/jquery.payment/blob/master/LICENSE" 20 | } 21 | ], 22 | "homepage": "https://github.com/stripe/jquery.payment", 23 | "docs": "https://github.com/stripe/jquery.payment", 24 | "bugs": "https://github.com/stripe/jquery.payment/issues", 25 | "demo": "http://stripe.github.io/jquery.payment/example", 26 | "dependencies": { 27 | "jquery": ">=1.7" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/jquery.payment.coffee: -------------------------------------------------------------------------------- 1 | $ = window.jQuery or window.Zepto or window.$ 2 | $.payment = {} 3 | $.payment.fn = {} 4 | $.fn.payment = (method, args...) -> 5 | $.payment.fn[method].apply(this, args) 6 | 7 | # Utils 8 | 9 | defaultFormat = /(\d{1,4})/g 10 | 11 | $.payment.cards = cards = [ 12 | { 13 | type: 'maestro' 14 | patterns: [ 15 | 5018, 502, 503, 506, 56, 58, 639, 6220, 67 16 | ] 17 | format: defaultFormat 18 | length: [12..19] 19 | cvcLength: [3] 20 | luhn: true 21 | } 22 | { 23 | type: 'forbrugsforeningen' 24 | patterns: [600] 25 | format: defaultFormat 26 | length: [16] 27 | cvcLength: [3] 28 | luhn: true 29 | } 30 | { 31 | type: 'dankort' 32 | patterns: [5019] 33 | format: defaultFormat 34 | length: [16] 35 | cvcLength: [3] 36 | luhn: true 37 | } 38 | # Credit cards 39 | { 40 | type: 'visa' 41 | patterns: [4] 42 | format: defaultFormat 43 | length: [13, 16] 44 | cvcLength: [3] 45 | luhn: true 46 | } 47 | { 48 | type: 'mastercard' 49 | patterns: [ 50 | 51, 52, 53, 54, 55, 51 | 22, 23, 24, 25, 26, 27 52 | ] 53 | format: defaultFormat 54 | length: [16] 55 | cvcLength: [3] 56 | luhn: true 57 | } 58 | { 59 | type: 'amex' 60 | patterns: [34, 37] 61 | format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/ 62 | length: [15] 63 | cvcLength: [3..4] 64 | luhn: true 65 | } 66 | { 67 | type: 'dinersclub' 68 | patterns: [30, 36, 38, 39] 69 | format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/ 70 | length: [14] 71 | cvcLength: [3] 72 | luhn: true 73 | } 74 | { 75 | type: 'discover' 76 | patterns: [60, 64, 65, 622] 77 | format: defaultFormat 78 | length: [16] 79 | cvcLength: [3] 80 | luhn: true 81 | } 82 | { 83 | type: 'unionpay' 84 | patterns: [62, 88] 85 | format: defaultFormat 86 | length: [16..19] 87 | cvcLength: [3] 88 | luhn: false 89 | } 90 | { 91 | type: 'jcb' 92 | patterns: [35] 93 | format: defaultFormat 94 | length: [16] 95 | cvcLength: [3] 96 | luhn: true 97 | } 98 | ] 99 | 100 | cardFromNumber = (num) -> 101 | num = (num + '').replace(/\D/g, '') 102 | for card in cards 103 | for pattern in card.patterns 104 | p = pattern + '' 105 | return card if num.substr(0, p.length) == p 106 | 107 | cardFromType = (type) -> 108 | return card for card in cards when card.type is type 109 | 110 | luhnCheck = (num) -> 111 | odd = true 112 | sum = 0 113 | 114 | digits = (num + '').split('').reverse() 115 | 116 | for digit in digits 117 | digit = parseInt(digit, 10) 118 | digit *= 2 if (odd = !odd) 119 | digit -= 9 if digit > 9 120 | sum += digit 121 | 122 | sum % 10 == 0 123 | 124 | hasTextSelected = ($target) -> 125 | # If some text is selected 126 | return true if $target.prop('selectionStart')? and 127 | $target.prop('selectionStart') isnt $target.prop('selectionEnd') 128 | 129 | # If some text is selected in IE 130 | if document?.selection?.createRange? 131 | return true if document.selection.createRange().text 132 | 133 | false 134 | 135 | # Private 136 | 137 | # Safe Val 138 | 139 | safeVal = (value, $target) -> 140 | try 141 | cursor = $target.prop('selectionStart') 142 | catch error 143 | cursor = null 144 | last = $target.val() 145 | $target.val(value) 146 | if cursor != null && $target.is(":focus") 147 | cursor = value.length if cursor is last.length 148 | 149 | # This hack looks for scenarios where we are changing an input's value such 150 | # that "X| " is replaced with " |X" (where "|" is the cursor). In those 151 | # scenarios, we want " X|". 152 | # 153 | # For example: 154 | # 1. Input field has value "4444| " 155 | # 2. User types "1" 156 | # 3. Input field has value "44441| " 157 | # 4. Reformatter changes it to "4444 |1" 158 | # 5. By incrementing the cursor, we make it "4444 1|" 159 | # 160 | # This is awful, and ideally doesn't go here, but given the current design 161 | # of the system there does not appear to be a better solution. 162 | # 163 | # Note that we can't just detect when the cursor-1 is " ", because that 164 | # would incorrectly increment the cursor when backspacing, e.g. pressing 165 | # backspace in this scenario: "4444 1|234 5". 166 | if last != value 167 | prevPair = last[cursor-1..cursor] 168 | currPair = value[cursor-1..cursor] 169 | digit = value[cursor] 170 | cursor = cursor + 1 if /\d/.test(digit) and 171 | prevPair == "#{digit} " and currPair == " #{digit}" 172 | 173 | $target.prop('selectionStart', cursor) 174 | $target.prop('selectionEnd', cursor) 175 | 176 | # Replace Full-Width Chars 177 | 178 | replaceFullWidthChars = (str = '') -> 179 | fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19' 180 | halfWidth = '0123456789' 181 | 182 | value = '' 183 | chars = str.split('') 184 | 185 | # Avoid using reserved word `char` 186 | for chr in chars 187 | idx = fullWidth.indexOf(chr) 188 | chr = halfWidth[idx] if idx > -1 189 | value += chr 190 | 191 | value 192 | 193 | # Format Numeric 194 | 195 | reFormatNumeric = (e) -> 196 | $target = $(e.currentTarget) 197 | setTimeout -> 198 | value = $target.val() 199 | value = replaceFullWidthChars(value) 200 | value = value.replace(/\D/g, '') 201 | safeVal(value, $target) 202 | 203 | # Format Card Number 204 | 205 | reFormatCardNumber = (e) -> 206 | $target = $(e.currentTarget) 207 | setTimeout -> 208 | value = $target.val() 209 | value = replaceFullWidthChars(value) 210 | value = $.payment.formatCardNumber(value) 211 | safeVal(value, $target) 212 | 213 | formatCardNumber = (e) -> 214 | # Only format if input is a number 215 | digit = String.fromCharCode(e.which) 216 | return unless /^\d+$/.test(digit) 217 | 218 | $target = $(e.currentTarget) 219 | value = $target.val() 220 | card = cardFromNumber(value + digit) 221 | length = (value.replace(/\D/g, '') + digit).length 222 | 223 | upperLength = 16 224 | upperLength = card.length[card.length.length - 1] if card 225 | return if length >= upperLength 226 | 227 | # Return if focus isn't at the end of the text 228 | return if $target.prop('selectionStart')? and 229 | $target.prop('selectionStart') isnt value.length 230 | 231 | if card && card.type is 'amex' 232 | # AMEX cards are formatted differently 233 | re = /^(\d{4}|\d{4}\s\d{6})$/ 234 | else 235 | re = /(?:^|\s)(\d{4})$/ 236 | 237 | # If '4242' + 4 238 | if re.test(value) 239 | e.preventDefault() 240 | setTimeout -> $target.val(value + ' ' + digit) 241 | 242 | # If '424' + 2 243 | else if re.test(value + digit) 244 | e.preventDefault() 245 | setTimeout -> $target.val(value + digit + ' ') 246 | 247 | formatBackCardNumber = (e) -> 248 | $target = $(e.currentTarget) 249 | value = $target.val() 250 | 251 | # Return unless backspacing 252 | return unless e.which is 8 253 | 254 | # Return if focus isn't at the end of the text 255 | return if $target.prop('selectionStart')? and 256 | $target.prop('selectionStart') isnt value.length 257 | 258 | # Remove the digit + trailing space 259 | if /\d\s$/.test(value) 260 | e.preventDefault() 261 | setTimeout -> $target.val(value.replace(/\d\s$/, '')) 262 | # Remove digit if ends in space + digit 263 | else if /\s\d?$/.test(value) 264 | e.preventDefault() 265 | setTimeout -> $target.val(value.replace(/\d$/, '')) 266 | 267 | # Format Expiry 268 | 269 | reFormatExpiry = (e) -> 270 | $target = $(e.currentTarget) 271 | setTimeout -> 272 | value = $target.val() 273 | value = replaceFullWidthChars(value) 274 | value = $.payment.formatExpiry(value) 275 | safeVal(value, $target) 276 | 277 | formatExpiry = (e) -> 278 | # Only format if input is a number 279 | digit = String.fromCharCode(e.which) 280 | return unless /^\d+$/.test(digit) 281 | 282 | $target = $(e.currentTarget) 283 | val = $target.val() + digit 284 | 285 | if /^\d$/.test(val) and val not in ['0', '1'] 286 | e.preventDefault() 287 | setTimeout -> $target.val("0#{val} / ") 288 | 289 | else if /^\d\d$/.test(val) 290 | e.preventDefault() 291 | setTimeout -> 292 | # Split for months where we have the second digit > 2 (past 12) and turn 293 | # that into (m1)(m2) => 0(m1) / (m2) 294 | m1 = parseInt(val[0], 10) 295 | m2 = parseInt(val[1], 10) 296 | if m2 > 2 and m1 != 0 297 | $target.val("0#{m1} / #{m2}") 298 | else 299 | $target.val("#{val} / ") 300 | 301 | formatForwardExpiry = (e) -> 302 | digit = String.fromCharCode(e.which) 303 | return unless /^\d+$/.test(digit) 304 | 305 | $target = $(e.currentTarget) 306 | val = $target.val() 307 | 308 | if /^\d\d$/.test(val) 309 | $target.val("#{val} / ") 310 | 311 | formatForwardSlashAndSpace = (e) -> 312 | which = String.fromCharCode(e.which) 313 | return unless which is '/' or which is ' ' 314 | 315 | $target = $(e.currentTarget) 316 | val = $target.val() 317 | 318 | if /^\d$/.test(val) and val isnt '0' 319 | $target.val("0#{val} / ") 320 | 321 | formatBackExpiry = (e) -> 322 | $target = $(e.currentTarget) 323 | value = $target.val() 324 | 325 | # Return unless backspacing 326 | return unless e.which is 8 327 | 328 | # Return if focus isn't at the end of the text 329 | return if $target.prop('selectionStart')? and 330 | $target.prop('selectionStart') isnt value.length 331 | 332 | # Remove the trailing space + last digit 333 | if /\d\s\/\s$/.test(value) 334 | e.preventDefault() 335 | setTimeout -> $target.val(value.replace(/\d\s\/\s$/, '')) 336 | 337 | # Format CVC 338 | 339 | reFormatCVC = (e) -> 340 | $target = $(e.currentTarget) 341 | setTimeout -> 342 | value = $target.val() 343 | value = replaceFullWidthChars(value) 344 | value = value.replace(/\D/g, '')[0...4] 345 | safeVal(value, $target) 346 | 347 | # Restrictions 348 | 349 | restrictNumeric = (e) -> 350 | # Key event is for a browser shortcut 351 | return true if e.metaKey or e.ctrlKey 352 | 353 | # If keycode is a space 354 | return false if e.which is 32 355 | 356 | # If keycode is a special char (WebKit) 357 | return true if e.which is 0 358 | 359 | # If char is a special char (Firefox) 360 | return true if e.which < 33 361 | 362 | input = String.fromCharCode(e.which) 363 | 364 | # Char is a number or a space 365 | !!/[\d\s]/.test(input) 366 | 367 | restrictCardNumber = (e) -> 368 | $target = $(e.currentTarget) 369 | digit = String.fromCharCode(e.which) 370 | return unless /^\d+$/.test(digit) 371 | 372 | return if hasTextSelected($target) 373 | 374 | # Restrict number of digits 375 | value = ($target.val() + digit).replace(/\D/g, '') 376 | card = cardFromNumber(value) 377 | 378 | if card 379 | value.length <= card.length[card.length.length - 1] 380 | else 381 | # All other cards are 16 digits long 382 | value.length <= 16 383 | 384 | restrictExpiry = (e) -> 385 | $target = $(e.currentTarget) 386 | digit = String.fromCharCode(e.which) 387 | return unless /^\d+$/.test(digit) 388 | 389 | return if hasTextSelected($target) 390 | 391 | value = $target.val() + digit 392 | value = value.replace(/\D/g, '') 393 | 394 | return false if value.length > 6 395 | 396 | restrictCVC = (e) -> 397 | $target = $(e.currentTarget) 398 | digit = String.fromCharCode(e.which) 399 | return unless /^\d+$/.test(digit) 400 | 401 | return if hasTextSelected($target) 402 | 403 | val = $target.val() + digit 404 | val.length <= 4 405 | 406 | setCardType = (e) -> 407 | $target = $(e.currentTarget) 408 | val = $target.val() 409 | cardType = $.payment.cardType(val) or 'unknown' 410 | 411 | unless $target.hasClass(cardType) 412 | allTypes = (card.type for card in cards) 413 | 414 | $target.removeClass('unknown') 415 | $target.removeClass(allTypes.join(' ')) 416 | 417 | $target.addClass(cardType) 418 | $target.toggleClass('identified', cardType isnt 'unknown') 419 | $target.trigger('payment.cardType', cardType) 420 | 421 | # Public 422 | 423 | # Formatting 424 | 425 | $.payment.fn.formatCardCVC = -> 426 | @on('keypress', restrictNumeric) 427 | @on('keypress', restrictCVC) 428 | @on('paste', reFormatCVC) 429 | @on('change', reFormatCVC) 430 | @on('input', reFormatCVC) 431 | this 432 | 433 | $.payment.fn.formatCardExpiry = -> 434 | @on('keypress', restrictNumeric) 435 | @on('keypress', restrictExpiry) 436 | @on('keypress', formatExpiry) 437 | @on('keypress', formatForwardSlashAndSpace) 438 | @on('keypress', formatForwardExpiry) 439 | @on('keydown', formatBackExpiry) 440 | @on('change', reFormatExpiry) 441 | @on('input', reFormatExpiry) 442 | this 443 | 444 | $.payment.fn.formatCardNumber = -> 445 | @on('keypress', restrictNumeric) 446 | @on('keypress', restrictCardNumber) 447 | @on('keypress', formatCardNumber) 448 | @on('keydown', formatBackCardNumber) 449 | @on('keyup', setCardType) 450 | @on('paste', reFormatCardNumber) 451 | @on('change', reFormatCardNumber) 452 | @on('input', reFormatCardNumber) 453 | @on('input', setCardType) 454 | this 455 | 456 | # Restrictions 457 | 458 | $.payment.fn.restrictNumeric = -> 459 | @on('keypress', restrictNumeric) 460 | @on('paste', reFormatNumeric) 461 | @on('change', reFormatNumeric) 462 | @on('input', reFormatNumeric) 463 | this 464 | 465 | # Validations 466 | 467 | $.payment.fn.cardExpiryVal = -> 468 | $.payment.cardExpiryVal($(this).val()) 469 | 470 | $.payment.cardExpiryVal = (value) -> 471 | [month, year] = value.split(/[\s\/]+/, 2) 472 | 473 | # Allow for year shortcut 474 | if year?.length is 2 and /^\d+$/.test(year) 475 | prefix = (new Date).getFullYear() 476 | prefix = prefix.toString()[0..1] 477 | year = prefix + year 478 | 479 | month = parseInt(month, 10) 480 | year = parseInt(year, 10) 481 | 482 | month: month, year: year 483 | 484 | $.payment.validateCardNumber = (num) -> 485 | num = (num + '').replace(/\s+|-/g, '') 486 | return false unless /^\d+$/.test(num) 487 | 488 | card = cardFromNumber(num) 489 | return false unless card 490 | 491 | num.length in card.length and 492 | (card.luhn is false or luhnCheck(num)) 493 | 494 | $.payment.validateCardExpiry = (month, year) -> 495 | # Allow passing an object 496 | if typeof month is 'object' and 'month' of month 497 | {month, year} = month 498 | 499 | return false unless month and year 500 | 501 | month = $.trim(month) 502 | year = $.trim(year) 503 | 504 | return false unless /^\d+$/.test(month) 505 | return false unless /^\d+$/.test(year) 506 | return false unless 1 <= month <= 12 507 | 508 | if year.length == 2 509 | if year < 70 510 | year = "20#{year}" 511 | else 512 | year = "19#{year}" 513 | 514 | return false unless year.length == 4 515 | 516 | expiry = new Date(year, month) 517 | currentTime = new Date 518 | 519 | # Months start from 0 in JavaScript 520 | expiry.setMonth(expiry.getMonth() - 1) 521 | 522 | # The cc expires at the end of the month, 523 | # so we need to make the expiry the first day 524 | # of the month after 525 | expiry.setMonth(expiry.getMonth() + 1, 1) 526 | 527 | expiry > currentTime 528 | 529 | $.payment.validateCardCVC = (cvc, type) -> 530 | cvc = $.trim(cvc) 531 | return false unless /^\d+$/.test(cvc) 532 | 533 | card = cardFromType(type) 534 | if card? 535 | # Check against a explicit card type 536 | cvc.length in card.cvcLength 537 | else 538 | # Check against all types 539 | cvc.length >= 3 and cvc.length <= 4 540 | 541 | $.payment.cardType = (num) -> 542 | return null unless num 543 | cardFromNumber(num)?.type or null 544 | 545 | $.payment.formatCardNumber = (num) -> 546 | num = num.replace(/\D/g, '') 547 | card = cardFromNumber(num) 548 | return num unless card 549 | 550 | upperLength = card.length[card.length.length - 1] 551 | num = num[0...upperLength] 552 | 553 | if card.format.global 554 | num.match(card.format)?.join(' ') 555 | else 556 | groups = card.format.exec(num) 557 | return unless groups? 558 | groups.shift() 559 | groups = $.grep(groups, (n) -> n) # Filter empty groups 560 | groups.join(' ') 561 | 562 | $.payment.formatExpiry = (expiry) -> 563 | parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/) 564 | return '' unless parts 565 | 566 | mon = parts[1] || '' 567 | sep = parts[2] || '' 568 | year = parts[3] || '' 569 | 570 | if year.length > 0 571 | sep = ' / ' 572 | 573 | else if sep is ' /' 574 | mon = mon.substring(0, 1) 575 | sep = '' 576 | 577 | else if mon.length == 2 or sep.length > 0 578 | sep = ' / ' 579 | 580 | else if mon.length == 1 and mon not in ['0', '1'] 581 | mon = "0#{mon}" 582 | sep = ' / ' 583 | 584 | return mon + sep + year 585 | -------------------------------------------------------------------------------- /test/jquery.coffee: -------------------------------------------------------------------------------- 1 | window = require('jsdom').jsdom().defaultView 2 | global.$ = require('jquery')(window) 3 | global.window = window 4 | global.document = window.document 5 | 6 | require('../src/jquery.payment') 7 | require('./specs') 8 | -------------------------------------------------------------------------------- /test/specs.coffee: -------------------------------------------------------------------------------- 1 | assert = require('assert') 2 | 3 | describe 'jquery.payment', -> 4 | describe 'Validating a card number', -> 5 | it 'should fail if empty', -> 6 | topic = $.payment.validateCardNumber '' 7 | assert.equal topic, false 8 | 9 | it 'should fail if is a bunch of spaces', -> 10 | topic = $.payment.validateCardNumber ' ' 11 | assert.equal topic, false 12 | 13 | it 'should success if is valid', -> 14 | topic = $.payment.validateCardNumber '4242424242424242' 15 | assert.equal topic, true 16 | 17 | it 'that has dashes in it but is valid', -> 18 | topic = $.payment.validateCardNumber '4242-4242-4242-4242' 19 | assert.equal topic, true 20 | 21 | it 'should succeed if it has spaces in it but is valid', -> 22 | topic = $.payment.validateCardNumber '4242 4242 4242 4242' 23 | assert.equal topic, true 24 | 25 | it 'that does not pass the luhn checker', -> 26 | topic = $.payment.validateCardNumber '4242424242424241' 27 | assert.equal topic, false 28 | 29 | it 'should fail if is more than 16 digits', -> 30 | topic = $.payment.validateCardNumber '42424242424242424' 31 | assert.equal topic, false 32 | 33 | it 'should fail if is less than 10 digits', -> 34 | topic = $.payment.validateCardNumber '424242424' 35 | assert.equal topic, false 36 | 37 | it 'should fail with non-digits', -> 38 | topic = $.payment.validateCardNumber '4242424e42424241' 39 | assert.equal topic, false 40 | 41 | it 'should validate for all card types', -> 42 | assert($.payment.validateCardNumber('6759649826438453'), 'maestro') 43 | 44 | assert($.payment.validateCardNumber('6007220000000004'), 'forbrugsforeningen') 45 | 46 | assert($.payment.validateCardNumber('5019717010103742'), 'dankort') 47 | 48 | assert($.payment.validateCardNumber('4111111111111111'), 'visa') 49 | assert($.payment.validateCardNumber('4012888888881881'), 'visa') 50 | assert($.payment.validateCardNumber('4222222222222'), 'visa') 51 | assert($.payment.validateCardNumber('4462030000000000'), 'visa') 52 | assert($.payment.validateCardNumber('4484070000000000'), 'visa') 53 | 54 | assert($.payment.validateCardNumber('5555555555554444'), 'mastercard') 55 | assert($.payment.validateCardNumber('5454545454545454'), 'mastercard') 56 | assert($.payment.validateCardNumber('2221000002222221'), 'mastercard') 57 | 58 | assert($.payment.validateCardNumber('378282246310005'), 'amex') 59 | assert($.payment.validateCardNumber('371449635398431'), 'amex') 60 | assert($.payment.validateCardNumber('378734493671000'), 'amex') 61 | 62 | assert($.payment.validateCardNumber('30569309025904'), 'dinersclub') 63 | assert($.payment.validateCardNumber('38520000023237'), 'dinersclub') 64 | assert($.payment.validateCardNumber('36700102000000'), 'dinersclub') 65 | assert($.payment.validateCardNumber('36148900647913'), 'dinersclub') 66 | 67 | assert($.payment.validateCardNumber('6011111111111117'), 'discover') 68 | assert($.payment.validateCardNumber('6011000990139424'), 'discover') 69 | 70 | assert($.payment.validateCardNumber('6271136264806203568'), 'unionpay') 71 | assert($.payment.validateCardNumber('6236265930072952775'), 'unionpay') 72 | assert($.payment.validateCardNumber('6204679475679144515'), 'unionpay') 73 | assert($.payment.validateCardNumber('6216657720782466507'), 'unionpay') 74 | 75 | assert($.payment.validateCardNumber('3530111333300000'), 'jcb') 76 | assert($.payment.validateCardNumber('3566002020360505'), 'jcb') 77 | 78 | describe 'Validating a CVC', -> 79 | it 'should fail if is empty', -> 80 | topic = $.payment.validateCardCVC '' 81 | assert.equal topic, false 82 | 83 | it 'should pass if is valid', -> 84 | topic = $.payment.validateCardCVC '123' 85 | assert.equal topic, true 86 | 87 | it 'should fail with non-digits', -> 88 | topic = $.payment.validateCardNumber '12e' 89 | assert.equal topic, false 90 | 91 | it 'should fail with less than 3 digits', -> 92 | topic = $.payment.validateCardNumber '12' 93 | assert.equal topic, false 94 | 95 | it 'should fail with more than 4 digits', -> 96 | topic = $.payment.validateCardNumber '12345' 97 | assert.equal topic, false 98 | 99 | describe 'Validating an expiration date', -> 100 | it 'should fail expires is before the current year', -> 101 | currentTime = new Date() 102 | topic = $.payment.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear() - 1 103 | assert.equal topic, false 104 | 105 | it 'that expires in the current year but before current month', -> 106 | currentTime = new Date() 107 | topic = $.payment.validateCardExpiry currentTime.getMonth(), currentTime.getFullYear() 108 | assert.equal topic, false 109 | 110 | it 'that has an invalid month', -> 111 | currentTime = new Date() 112 | topic = $.payment.validateCardExpiry 13, currentTime.getFullYear() 113 | assert.equal topic, false 114 | 115 | it 'that is this year and month', -> 116 | currentTime = new Date() 117 | topic = $.payment.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear() 118 | assert.equal topic, true 119 | 120 | it 'that is just after this month', -> 121 | # Remember - months start with 0 in JavaScript! 122 | currentTime = new Date() 123 | topic = $.payment.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear() 124 | assert.equal topic, true 125 | 126 | it 'that is after this year', -> 127 | currentTime = new Date() 128 | topic = $.payment.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear() + 1 129 | assert.equal topic, true 130 | 131 | it 'that is a two-digit year', -> 132 | currentTime = new Date() 133 | topic = $.payment.validateCardExpiry currentTime.getMonth() + 1, ('' + currentTime.getFullYear())[0...2] 134 | assert.equal topic, true 135 | 136 | it 'that is a two-digit year in the past (i.e. 1990s)', -> 137 | currentTime = new Date() 138 | topic = $.payment.validateCardExpiry currentTime.getMonth() + 1, 99 139 | assert.equal topic, false 140 | 141 | it 'that has string numbers', -> 142 | currentTime = new Date() 143 | currentTime.setFullYear(currentTime.getFullYear() + 1, currentTime.getMonth() + 2) 144 | topic = $.payment.validateCardExpiry currentTime.getMonth() + 1 + '', currentTime.getFullYear() + '' 145 | assert.equal topic, true 146 | 147 | it 'that has non-numbers', -> 148 | topic = $.payment.validateCardExpiry 'h12', '3300' 149 | assert.equal topic, false 150 | 151 | it 'should fail if year or month is NaN', -> 152 | topic = $.payment.validateCardExpiry '12', NaN 153 | assert.equal topic, false 154 | 155 | it 'should support year shorthand', -> 156 | assert.equal $.payment.validateCardExpiry('05', '20'), true 157 | 158 | describe 'Validating a CVC number', -> 159 | it 'should validate a three digit number with no card type', -> 160 | topic = $.payment.validateCardCVC('123') 161 | assert.equal topic, true 162 | 163 | it 'should validate a three digit number with card type amex', -> 164 | topic = $.payment.validateCardCVC('123', 'amex') 165 | assert.equal topic, true 166 | 167 | it 'should validate a three digit number with card type other than amex', -> 168 | topic = $.payment.validateCardCVC('123', 'visa') 169 | assert.equal topic, true 170 | 171 | it 'should not validate a four digit number with a card type other than amex', -> 172 | topic = $.payment.validateCardCVC('1234', 'visa') 173 | assert.equal topic, false 174 | 175 | it 'should validate a four digit number with card type amex', -> 176 | topic = $.payment.validateCardCVC('1234', 'amex') 177 | assert.equal topic, true 178 | 179 | it 'should not validate a number larger than 4 digits', -> 180 | topic = $.payment.validateCardCVC('12344') 181 | assert.equal topic, false 182 | 183 | describe 'Parsing an expiry value', -> 184 | it 'should parse string expiry', -> 185 | topic = $.payment.cardExpiryVal('03 / 2025') 186 | assert.deepEqual topic, month: 3, year: 2025 187 | 188 | it 'should support shorthand year', -> 189 | topic = $.payment.cardExpiryVal('05/04') 190 | assert.deepEqual topic, month: 5, year: 2004 191 | 192 | it 'should return NaN when it cannot parse', -> 193 | topic = $.payment.cardExpiryVal('05/dd') 194 | assert isNaN(topic.year) 195 | 196 | describe 'Getting a card type', -> 197 | it 'should return Visa that begins with 40', -> 198 | topic = $.payment.cardType '4012121212121212' 199 | assert.equal topic, 'visa' 200 | 201 | it 'that begins with 2 should return MasterCard', -> 202 | topic = $.payment.cardType '2221000002222221' 203 | assert.equal topic, 'mastercard' 204 | 205 | it 'that begins with 5 should return MasterCard', -> 206 | topic = $.payment.cardType '5555555555554444' 207 | assert.equal topic, 'mastercard' 208 | 209 | it 'that begins with 34 should return American Express', -> 210 | topic = $.payment.cardType '3412121212121212' 211 | assert.equal topic, 'amex' 212 | 213 | it 'that is not numbers should return null', -> 214 | topic = $.payment.cardType 'aoeu' 215 | assert.equal topic, null 216 | 217 | it 'that has unrecognized beginning numbers should return null', -> 218 | topic = $.payment.cardType 'aoeu' 219 | assert.equal topic, null 220 | 221 | it 'should return correct type for all test numbers', -> 222 | assert.equal($.payment.cardType('6759649826438453'), 'maestro') 223 | assert.equal($.payment.cardType('6220180012340012345'), 'maestro') 224 | 225 | assert.equal($.payment.cardType('6007220000000004'), 'forbrugsforeningen') 226 | 227 | assert.equal($.payment.cardType('5019717010103742'), 'dankort') 228 | 229 | assert.equal($.payment.cardType('4111111111111111'), 'visa') 230 | assert.equal($.payment.cardType('4012888888881881'), 'visa') 231 | assert.equal($.payment.cardType('4222222222222'), 'visa') 232 | assert.equal($.payment.cardType('4462030000000000'), 'visa') 233 | assert.equal($.payment.cardType('4484070000000000'), 'visa') 234 | 235 | assert.equal($.payment.cardType('5555555555554444'), 'mastercard') 236 | assert.equal($.payment.cardType('5454545454545454'), 'mastercard') 237 | assert.equal($.payment.cardType('2221000002222221'), 'mastercard') 238 | 239 | assert.equal($.payment.cardType('378282246310005'), 'amex') 240 | assert.equal($.payment.cardType('371449635398431'), 'amex') 241 | assert.equal($.payment.cardType('378734493671000'), 'amex') 242 | 243 | assert.equal($.payment.cardType('30569309025904'), 'dinersclub') 244 | assert.equal($.payment.cardType('38520000023237'), 'dinersclub') 245 | assert.equal($.payment.cardType('36700102000000'), 'dinersclub') 246 | assert.equal($.payment.cardType('36148900647913'), 'dinersclub') 247 | 248 | assert.equal($.payment.cardType('6011111111111117'), 'discover') 249 | assert.equal($.payment.cardType('6011000990139424'), 'discover') 250 | 251 | assert.equal($.payment.cardType('6271136264806203568'), 'unionpay') 252 | assert.equal($.payment.cardType('6236265930072952775'), 'unionpay') 253 | assert.equal($.payment.cardType('6204679475679144515'), 'unionpay') 254 | assert.equal($.payment.cardType('6216657720782466507'), 'unionpay') 255 | 256 | assert.equal($.payment.cardType('3530111333300000'), 'jcb') 257 | assert.equal($.payment.cardType('3566002020360505'), 'jcb') 258 | 259 | describe 'Extending the card collection', -> 260 | it 'should expose an array of standard card types', -> 261 | cards = $.payment.cards 262 | assert Array.isArray(cards) 263 | 264 | visa = card for card in cards when card.type is 'visa' 265 | assert.notEqual visa, null 266 | 267 | it 'should support new card types', -> 268 | wing = { 269 | type: 'wing' 270 | patterns: [501818] 271 | length: [16] 272 | luhn: false 273 | } 274 | $.payment.cards.unshift wing 275 | 276 | wingCard = '5018 1818 1818 1818' 277 | assert.equal $.payment.cardType(wingCard), 'wing' 278 | assert.equal $.payment.validateCardNumber(wingCard), true 279 | 280 | describe 'formatCardNumber', -> 281 | it 'should format cc number correctly', (done) -> 282 | $number = $('').payment('formatCardNumber') 283 | $number.val('4242').prop('selectionStart', 4) 284 | 285 | e = $.Event('keypress') 286 | e.which = 52 # '4' 287 | $number.trigger(e) 288 | 289 | setTimeout -> 290 | assert.equal $number.val(), '4242 4' 291 | done() 292 | 293 | it 'should format amex cc number correctly', (done) -> 294 | $number = $('').payment('formatCardNumber') 295 | $number.val('3782').prop('selectionStart', 4) 296 | 297 | e = $.Event('keypress') 298 | e.which = 56 # '8' 299 | $number.trigger(e) 300 | 301 | setTimeout -> 302 | assert.equal $number.val(), '3782 8' 303 | done() 304 | 305 | it 'should format full-width cc number correctly', (done) -> 306 | $number = $('').payment('formatCardNumber') 307 | $number.val('\uff14\uff12\uff14\uff12') 308 | 309 | e = $.Event('input') 310 | $number.trigger(e) 311 | 312 | setTimeout -> 313 | assert.equal $number.val(), '4242' 314 | done() 315 | 316 | describe 'formatCardExpiry', -> 317 | it 'should format month shorthand correctly', (done) -> 318 | $expiry = $('').payment('formatCardExpiry') 319 | $expiry.val('') 320 | 321 | e = $.Event('keypress') 322 | e.which = 52 # '4' 323 | $expiry.trigger(e) 324 | 325 | setTimeout -> 326 | assert.equal $expiry.val(), '04 / ' 327 | done() 328 | 329 | it 'should format forward slash shorthand correctly', (done) -> 330 | $expiry = $('').payment('formatCardExpiry') 331 | $expiry.val('1') 332 | 333 | e = $.Event('keypress') 334 | e.which = 47 # '/' 335 | $expiry.trigger(e) 336 | 337 | setTimeout -> 338 | assert.equal $expiry.val(), '01 / ' 339 | done() 340 | 341 | it 'should only allow numbers', (done) -> 342 | $expiry = $('').payment('formatCardExpiry') 343 | $expiry.val('1') 344 | 345 | e = $.Event('keypress') 346 | e.which = 100 # 'd' 347 | $expiry.trigger(e) 348 | 349 | setTimeout -> 350 | assert.equal $expiry.val(), '1' 351 | done() 352 | 353 | it 'should format full-width expiry correctly', (done) -> 354 | $expiry = $('').payment('formatCardExpiry') 355 | $expiry.val('\uff10\uff18\uff11\uff15') 356 | 357 | e = $.Event('input') 358 | $expiry.trigger(e) 359 | 360 | setTimeout -> 361 | assert.equal $expiry.val(), '08 / 15' 362 | done() 363 | 364 | it 'should format month expiry correctly when val is past 12', (done) -> 365 | $expiry = $('').payment('formatCardExpiry') 366 | $expiry.val('1') 367 | 368 | e = $.Event('keypress') 369 | e.which = 52 # '4' 370 | $expiry.trigger(e) 371 | 372 | setTimeout -> 373 | assert.equal $expiry.val(), '01 / 4' 374 | done() 375 | 376 | it 'should format month expiry corrrectly for 0 followed by single digit > 2', (done) -> 377 | $expiry = $('').payment('formatCardExpiry') 378 | $expiry.val('0') 379 | 380 | e = $.Event('keypress') 381 | e.which = 53 # '5' 382 | $expiry.trigger(e) 383 | 384 | setTimeout -> 385 | assert.equal $expiry.val(), '05 / ' 386 | done() 387 | -------------------------------------------------------------------------------- /test/zepto.coffee: -------------------------------------------------------------------------------- 1 | window = require('jsdom').jsdom().defaultView 2 | global.window = window 3 | global.document = window.document 4 | global.getComputedStyle = window.getComputedStyle 5 | global.$ = require('zeptojs') 6 | window.$ = global.$ 7 | 8 | require('../src/jquery.payment') 9 | require('./specs') 10 | --------------------------------------------------------------------------------