├── .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 [](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 |
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 |
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 |
--------------------------------------------------------------------------------