Added jquery.payment to make card-input easier

This commit is contained in:
Kaspar Emanuel 2016-06-17 20:06:51 +01:00
parent 5365b1172e
commit 4dcd992aa7
25 changed files with 2916 additions and 18 deletions

View File

@ -30,6 +30,7 @@ module.exports = function (grunt) {
'./public/vendor/jquery-form/jquery.form.js',
'./public/vendor/RRSSB/js/rrssb.js',
'./public/vendor/humane-js/humane.js',
'./public/vendor/jquery.payment/lib/jquery.payment.js',
'./public/assets/javascript/app-public.js'
],
dest: './public/assets/javascript/frontend.js',

View File

@ -31,7 +31,8 @@
"fullcalendar": "^2.7.1",
"vue": "^1.0.24",
"vue-resource": "^0.7.0",
"html.sortable": "^0.3.1"
"html.sortable": "^0.3.1",
"jquery.payment": "https://github.com/stripe/jquery.payment.git#^1.4.2"
},
"resolutions": {
"jquery": ">=1.5",

View File

@ -8,6 +8,7 @@
},
"devDependencies": {
"grunt": "^0.4.5",
"grunt-cli": "^1.2.0",
"grunt-contrib-concat": "^0.5.1",
"grunt-contrib-less": "^0.12.0",
"grunt-contrib-uglify": "^0.6.0",

View File

@ -83,9 +83,8 @@ $(function() {
toggleSubmitDisabled($submitButton);
if ($form.hasClass('payment-form')) {
clearFormErrors($('.payment-form'));
Stripe.setPublishableKey($form.data('stripe-pub-key'));
var
@ -96,6 +95,7 @@ $(function() {
$expiryMonth = $('.card-expiry-month'),
$expiryYear = $('.card-expiry-year');
if (!Stripe.validateCardNumber($cardNumber.val())) {
showFormError($cardNumber, 'The credit card number appears to be invalid.');
noErrors = false;
@ -131,7 +131,7 @@ $(function() {
$form.append($('<input type="hidden" name="stripeToken" />').val(token));
$form.ajaxSubmit(ajaxFormConf);
}
});
} else {
showMessage('Please check your card details and try again.');
@ -147,6 +147,7 @@ $(function() {
offset: -60
});
/* Scroll to top */
$(window).scroll(function() {
if ($(this).scrollTop() > 100) {
@ -172,6 +173,9 @@ $(function() {
$('.ticket_holder_email').val($('#order_email').val());
});
$('.card-number').payment('formatCardNumber');
$('.card-cvc').payment('formatCardCVC');
});
function processFormErrors($form, errors)
@ -201,7 +205,7 @@ function processFormErrors($form, errors)
/**
* Toggle a submit button disabled/enabled - duh!
*
*
* @param element $submitButton
* @returns void
*/
@ -222,7 +226,7 @@ function toggleSubmitDisabled($submitButton) {
/**
* Clears given form of any error classes / messages
*
*
* @param {Element} $form
* @returns {void}
*/
@ -247,7 +251,7 @@ function showFormError($formElement, message) {
/**
* Shows users a message.
* Currently uses humane.js
*
*
* @param string message
* @returns void
*/
@ -264,7 +268,7 @@ function hideMessage() {
/**
* Counts down to the given number of seconds
*
*
* @param element $element
* @param int seconds
* @returns void
@ -370,4 +374,4 @@ $.extend(
}, t.smoothScroll.version = l, t.smoothScroll.filterPath = function(t) {
return t.replace(/^\//, "").replace(/(?:index|default).[a-zA-Z]{3,4}$/, "").replace(/\/$/, "")
}, t.fn.smoothScroll.defaults = s
})(jQuery);
})(jQuery);

View File

@ -3901,6 +3901,671 @@ function log() {
}
return new Humane()
});
;(function() {
var $, cardFromNumber, cardFromType, cards, defaultFormat, formatBackCardNumber, formatBackExpiry, formatCardNumber, formatExpiry, formatForwardExpiry, formatForwardSlashAndSpace, hasTextSelected, luhnCheck, reFormatCVC, reFormatCardNumber, reFormatExpiry, reFormatNumeric, replaceFullWidthChars, restrictCVC, restrictCardNumber, restrictExpiry, restrictNumeric, safeVal, setCardType,
__slice = [].slice,
__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; };
$ = window.jQuery || window.Zepto || window.$;
$.payment = {};
$.payment.fn = {};
$.fn.payment = function() {
var args, method;
method = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
return $.payment.fn[method].apply(this, args);
};
defaultFormat = /(\d{1,4})/g;
$.payment.cards = cards = [
{
type: 'elo',
patterns: [4011, 4312, 4389, 4514, 4573, 4576, 5041, 5066, 5067, 509, 6277, 6362, 6363, 650, 6516, 6550],
format: defaultFormat,
length: [16],
cvcLength: [3],
luhn: true
}, {
type: 'visaelectron',
patterns: [4026, 417500, 4405, 4508, 4844, 4913, 4917],
format: defaultFormat,
length: [16],
cvcLength: [3],
luhn: true
}, {
type: 'maestro',
patterns: [5018, 502, 503, 506, 56, 58, 639, 6220, 67],
format: defaultFormat,
length: [12, 13, 14, 15, 16, 17, 18, 19],
cvcLength: [3],
luhn: true
}, {
type: 'forbrugsforeningen',
patterns: [600],
format: defaultFormat,
length: [16],
cvcLength: [3],
luhn: true
}, {
type: 'dankort',
patterns: [5019],
format: defaultFormat,
length: [16],
cvcLength: [3],
luhn: true
}, {
type: 'visa',
patterns: [4],
format: defaultFormat,
length: [13, 16],
cvcLength: [3],
luhn: true
}, {
type: 'mastercard',
patterns: [51, 52, 53, 54, 55, 22, 23, 24, 25, 26, 27],
format: defaultFormat,
length: [16],
cvcLength: [3],
luhn: true
}, {
type: 'amex',
patterns: [34, 37],
format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/,
length: [15],
cvcLength: [3, 4],
luhn: true
}, {
type: 'dinersclub',
patterns: [30, 36, 38, 39],
format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/,
length: [14],
cvcLength: [3],
luhn: true
}, {
type: 'discover',
patterns: [60, 64, 65, 622],
format: defaultFormat,
length: [16],
cvcLength: [3],
luhn: true
}, {
type: 'unionpay',
patterns: [62, 88],
format: defaultFormat,
length: [16, 17, 18, 19],
cvcLength: [3],
luhn: false
}, {
type: 'jcb',
patterns: [35],
format: defaultFormat,
length: [16],
cvcLength: [3],
luhn: true
}
];
cardFromNumber = function(num) {
var card, p, pattern, _i, _j, _len, _len1, _ref;
num = (num + '').replace(/\D/g, '');
for (_i = 0, _len = cards.length; _i < _len; _i++) {
card = cards[_i];
_ref = card.patterns;
for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) {
pattern = _ref[_j];
p = pattern + '';
if (num.substr(0, p.length) === p) {
return card;
}
}
}
};
cardFromType = function(type) {
var card, _i, _len;
for (_i = 0, _len = cards.length; _i < _len; _i++) {
card = cards[_i];
if (card.type === type) {
return card;
}
}
};
luhnCheck = function(num) {
var digit, digits, odd, sum, _i, _len;
odd = true;
sum = 0;
digits = (num + '').split('').reverse();
for (_i = 0, _len = digits.length; _i < _len; _i++) {
digit = digits[_i];
digit = parseInt(digit, 10);
if ((odd = !odd)) {
digit *= 2;
}
if (digit > 9) {
digit -= 9;
}
sum += digit;
}
return sum % 10 === 0;
};
hasTextSelected = function($target) {
var _ref;
if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== $target.prop('selectionEnd')) {
return true;
}
if ((typeof document !== "undefined" && document !== null ? (_ref = document.selection) != null ? _ref.createRange : void 0 : void 0) != null) {
if (document.selection.createRange().text) {
return true;
}
}
return false;
};
safeVal = function(value, $target) {
var currPair, cursor, digit, error, last, prevPair;
try {
cursor = $target.prop('selectionStart');
} catch (_error) {
error = _error;
cursor = null;
}
last = $target.val();
$target.val(value);
if (cursor !== null && $target.is(":focus")) {
if (cursor === last.length) {
cursor = value.length;
}
if (last !== value) {
prevPair = last.slice(cursor - 1, +cursor + 1 || 9e9);
currPair = value.slice(cursor - 1, +cursor + 1 || 9e9);
digit = value[cursor];
if (/\d/.test(digit) && prevPair === ("" + digit + " ") && currPair === (" " + digit)) {
cursor = cursor + 1;
}
}
$target.prop('selectionStart', cursor);
return $target.prop('selectionEnd', cursor);
}
};
replaceFullWidthChars = function(str) {
var chars, chr, fullWidth, halfWidth, idx, value, _i, _len;
if (str == null) {
str = '';
}
fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19';
halfWidth = '0123456789';
value = '';
chars = str.split('');
for (_i = 0, _len = chars.length; _i < _len; _i++) {
chr = chars[_i];
idx = fullWidth.indexOf(chr);
if (idx > -1) {
chr = halfWidth[idx];
}
value += chr;
}
return value;
};
reFormatNumeric = function(e) {
var $target;
$target = $(e.currentTarget);
return setTimeout(function() {
var value;
value = $target.val();
value = replaceFullWidthChars(value);
value = value.replace(/\D/g, '');
return safeVal(value, $target);
});
};
reFormatCardNumber = function(e) {
var $target;
$target = $(e.currentTarget);
return setTimeout(function() {
var value;
value = $target.val();
value = replaceFullWidthChars(value);
value = $.payment.formatCardNumber(value);
return safeVal(value, $target);
});
};
formatCardNumber = function(e) {
var $target, card, digit, length, re, upperLength, value;
digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) {
return;
}
$target = $(e.currentTarget);
value = $target.val();
card = cardFromNumber(value + digit);
length = (value.replace(/\D/g, '') + digit).length;
upperLength = 16;
if (card) {
upperLength = card.length[card.length.length - 1];
}
if (length >= upperLength) {
return;
}
if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
return;
}
if (card && card.type === 'amex') {
re = /^(\d{4}|\d{4}\s\d{6})$/;
} else {
re = /(?:^|\s)(\d{4})$/;
}
if (re.test(value)) {
e.preventDefault();
return setTimeout(function() {
return $target.val(value + ' ' + digit);
});
} else if (re.test(value + digit)) {
e.preventDefault();
return setTimeout(function() {
return $target.val(value + digit + ' ');
});
}
};
formatBackCardNumber = function(e) {
var $target, value;
$target = $(e.currentTarget);
value = $target.val();
if (e.which !== 8) {
return;
}
if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
return;
}
if (/\d\s$/.test(value)) {
e.preventDefault();
return setTimeout(function() {
return $target.val(value.replace(/\d\s$/, ''));
});
} else if (/\s\d?$/.test(value)) {
e.preventDefault();
return setTimeout(function() {
return $target.val(value.replace(/\d$/, ''));
});
}
};
reFormatExpiry = function(e) {
var $target;
$target = $(e.currentTarget);
return setTimeout(function() {
var value;
value = $target.val();
value = replaceFullWidthChars(value);
value = $.payment.formatExpiry(value);
return safeVal(value, $target);
});
};
formatExpiry = function(e) {
var $target, digit, val;
digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) {
return;
}
$target = $(e.currentTarget);
val = $target.val() + digit;
if (/^\d$/.test(val) && (val !== '0' && val !== '1')) {
e.preventDefault();
return setTimeout(function() {
return $target.val("0" + val + " / ");
});
} else if (/^\d\d$/.test(val)) {
e.preventDefault();
return setTimeout(function() {
var m1, m2;
m1 = parseInt(val[0], 10);
m2 = parseInt(val[1], 10);
if (m2 > 2 && m1 !== 0) {
return $target.val("0" + m1 + " / " + m2);
} else {
return $target.val("" + val + " / ");
}
});
}
};
formatForwardExpiry = function(e) {
var $target, digit, val;
digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) {
return;
}
$target = $(e.currentTarget);
val = $target.val();
if (/^\d\d$/.test(val)) {
return $target.val("" + val + " / ");
}
};
formatForwardSlashAndSpace = function(e) {
var $target, val, which;
which = String.fromCharCode(e.which);
if (!(which === '/' || which === ' ')) {
return;
}
$target = $(e.currentTarget);
val = $target.val();
if (/^\d$/.test(val) && val !== '0') {
return $target.val("0" + val + " / ");
}
};
formatBackExpiry = function(e) {
var $target, value;
$target = $(e.currentTarget);
value = $target.val();
if (e.which !== 8) {
return;
}
if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
return;
}
if (/\d\s\/\s$/.test(value)) {
e.preventDefault();
return setTimeout(function() {
return $target.val(value.replace(/\d\s\/\s$/, ''));
});
}
};
reFormatCVC = function(e) {
var $target;
$target = $(e.currentTarget);
return setTimeout(function() {
var value;
value = $target.val();
value = replaceFullWidthChars(value);
value = value.replace(/\D/g, '').slice(0, 4);
return safeVal(value, $target);
});
};
restrictNumeric = function(e) {
var input;
if (e.metaKey || e.ctrlKey) {
return true;
}
if (e.which === 32) {
return false;
}
if (e.which === 0) {
return true;
}
if (e.which < 33) {
return true;
}
input = String.fromCharCode(e.which);
return !!/[\d\s]/.test(input);
};
restrictCardNumber = function(e) {
var $target, card, digit, value;
$target = $(e.currentTarget);
digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) {
return;
}
if (hasTextSelected($target)) {
return;
}
value = ($target.val() + digit).replace(/\D/g, '');
card = cardFromNumber(value);
if (card) {
return value.length <= card.length[card.length.length - 1];
} else {
return value.length <= 16;
}
};
restrictExpiry = function(e) {
var $target, digit, value;
$target = $(e.currentTarget);
digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) {
return;
}
if (hasTextSelected($target)) {
return;
}
value = $target.val() + digit;
value = value.replace(/\D/g, '');
if (value.length > 6) {
return false;
}
};
restrictCVC = function(e) {
var $target, digit, val;
$target = $(e.currentTarget);
digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) {
return;
}
if (hasTextSelected($target)) {
return;
}
val = $target.val() + digit;
return val.length <= 4;
};
setCardType = function(e) {
var $target, allTypes, card, cardType, val;
$target = $(e.currentTarget);
val = $target.val();
cardType = $.payment.cardType(val) || 'unknown';
if (!$target.hasClass(cardType)) {
allTypes = (function() {
var _i, _len, _results;
_results = [];
for (_i = 0, _len = cards.length; _i < _len; _i++) {
card = cards[_i];
_results.push(card.type);
}
return _results;
})();
$target.removeClass('unknown');
$target.removeClass(allTypes.join(' '));
$target.addClass(cardType);
$target.toggleClass('identified', cardType !== 'unknown');
return $target.trigger('payment.cardType', cardType);
}
};
$.payment.fn.formatCardCVC = function() {
this.on('keypress', restrictNumeric);
this.on('keypress', restrictCVC);
this.on('paste', reFormatCVC);
this.on('change', reFormatCVC);
this.on('input', reFormatCVC);
return this;
};
$.payment.fn.formatCardExpiry = function() {
this.on('keypress', restrictNumeric);
this.on('keypress', restrictExpiry);
this.on('keypress', formatExpiry);
this.on('keypress', formatForwardSlashAndSpace);
this.on('keypress', formatForwardExpiry);
this.on('keydown', formatBackExpiry);
this.on('change', reFormatExpiry);
this.on('input', reFormatExpiry);
return this;
};
$.payment.fn.formatCardNumber = function() {
this.on('keypress', restrictNumeric);
this.on('keypress', restrictCardNumber);
this.on('keypress', formatCardNumber);
this.on('keydown', formatBackCardNumber);
this.on('keyup', setCardType);
this.on('paste', reFormatCardNumber);
this.on('change', reFormatCardNumber);
this.on('input', reFormatCardNumber);
this.on('input', setCardType);
return this;
};
$.payment.fn.restrictNumeric = function() {
this.on('keypress', restrictNumeric);
this.on('paste', reFormatNumeric);
this.on('change', reFormatNumeric);
this.on('input', reFormatNumeric);
return this;
};
$.payment.fn.cardExpiryVal = function() {
return $.payment.cardExpiryVal($(this).val());
};
$.payment.cardExpiryVal = function(value) {
var month, prefix, year, _ref;
_ref = value.split(/[\s\/]+/, 2), month = _ref[0], year = _ref[1];
if ((year != null ? year.length : void 0) === 2 && /^\d+$/.test(year)) {
prefix = (new Date).getFullYear();
prefix = prefix.toString().slice(0, 2);
year = prefix + year;
}
month = parseInt(month, 10);
year = parseInt(year, 10);
return {
month: month,
year: year
};
};
$.payment.validateCardNumber = function(num) {
var card, _ref;
num = (num + '').replace(/\s+|-/g, '');
if (!/^\d+$/.test(num)) {
return false;
}
card = cardFromNumber(num);
if (!card) {
return false;
}
return (_ref = num.length, __indexOf.call(card.length, _ref) >= 0) && (card.luhn === false || luhnCheck(num));
};
$.payment.validateCardExpiry = function(month, year) {
var currentTime, expiry, _ref;
if (typeof month === 'object' && 'month' in month) {
_ref = month, month = _ref.month, year = _ref.year;
}
if (!(month && year)) {
return false;
}
month = $.trim(month);
year = $.trim(year);
if (!/^\d+$/.test(month)) {
return false;
}
if (!/^\d+$/.test(year)) {
return false;
}
if (!((1 <= month && month <= 12))) {
return false;
}
if (year.length === 2) {
if (year < 70) {
year = "20" + year;
} else {
year = "19" + year;
}
}
if (year.length !== 4) {
return false;
}
expiry = new Date(year, month);
currentTime = new Date;
expiry.setMonth(expiry.getMonth() - 1);
expiry.setMonth(expiry.getMonth() + 1, 1);
return expiry > currentTime;
};
$.payment.validateCardCVC = function(cvc, type) {
var card, _ref;
cvc = $.trim(cvc);
if (!/^\d+$/.test(cvc)) {
return false;
}
card = cardFromType(type);
if (card != null) {
return _ref = cvc.length, __indexOf.call(card.cvcLength, _ref) >= 0;
} else {
return cvc.length >= 3 && cvc.length <= 4;
}
};
$.payment.cardType = function(num) {
var _ref;
if (!num) {
return null;
}
return ((_ref = cardFromNumber(num)) != null ? _ref.type : void 0) || null;
};
$.payment.formatCardNumber = function(num) {
var card, groups, upperLength, _ref;
num = num.replace(/\D/g, '');
card = cardFromNumber(num);
if (!card) {
return num;
}
upperLength = card.length[card.length.length - 1];
num = num.slice(0, upperLength);
if (card.format.global) {
return (_ref = num.match(card.format)) != null ? _ref.join(' ') : void 0;
} else {
groups = card.format.exec(num);
if (groups == null) {
return;
}
groups.shift();
groups = $.grep(groups, function(n) {
return n;
});
return groups.join(' ');
}
};
$.payment.formatExpiry = function(expiry) {
var mon, parts, sep, year;
parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/);
if (!parts) {
return '';
}
mon = parts[1] || '';
sep = parts[2] || '';
year = parts[3] || '';
if (year.length > 0) {
sep = ' / ';
} else if (sep === ' /') {
mon = mon.substring(0, 1);
sep = '';
} else if (mon.length === 2 || sep.length > 0) {
sep = ' / ';
} else if (mon.length === 1 && (mon !== '0' && mon !== '1')) {
mon = "0" + mon;
sep = ' / ';
}
return mon + sep + year;
};
}).call(this);
;$(function() {
$('form.ajax').on('submit', function(e) {
e.preventDefault();
@ -3986,9 +4651,8 @@ function log() {
toggleSubmitDisabled($submitButton);
if ($form.hasClass('payment-form')) {
clearFormErrors($('.payment-form'));
Stripe.setPublishableKey($form.data('stripe-pub-key'));
var
@ -3999,6 +4663,7 @@ function log() {
$expiryMonth = $('.card-expiry-month'),
$expiryYear = $('.card-expiry-year');
if (!Stripe.validateCardNumber($cardNumber.val())) {
showFormError($cardNumber, 'The credit card number appears to be invalid.');
noErrors = false;
@ -4034,7 +4699,7 @@ function log() {
$form.append($('<input type="hidden" name="stripeToken" />').val(token));
$form.ajaxSubmit(ajaxFormConf);
}
});
} else {
showMessage('Please check your card details and try again.');
@ -4050,6 +4715,7 @@ function log() {
offset: -60
});
/* Scroll to top */
$(window).scroll(function() {
if ($(this).scrollTop() > 100) {
@ -4075,6 +4741,9 @@ function log() {
$('.ticket_holder_email').val($('#order_email').val());
});
$('.card-number').payment('formatCardNumber');
$('.card-cvc').payment('formatCardCVC');
});
function processFormErrors($form, errors)
@ -4104,7 +4773,7 @@ function processFormErrors($form, errors)
/**
* Toggle a submit button disabled/enabled - duh!
*
*
* @param element $submitButton
* @returns void
*/
@ -4125,7 +4794,7 @@ function toggleSubmitDisabled($submitButton) {
/**
* Clears given form of any error classes / messages
*
*
* @param {Element} $form
* @returns {void}
*/
@ -4150,7 +4819,7 @@ function showFormError($formElement, message) {
/**
* Shows users a message.
* Currently uses humane.js
*
*
* @param string message
* @returns void
*/
@ -4167,7 +4836,7 @@ function hideMessage() {
/**
* Counts down to the given number of seconds
*
*
* @param element $element
* @param int seconds
* @returns void
@ -4273,4 +4942,4 @@ $.extend(
}, t.smoothScroll.version = l, t.smoothScroll.filterPath = function(t) {
return t.replace(/^\//, "").replace(/(?:index|default).[a-zA-Z]{3,4}$/, "").replace(/\/$/, "")
}, t.fn.smoothScroll.defaults = s
})(jQuery);
})(jQuery);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,19 @@
{
"name": "jquery.payment",
"main": "lib/jquery.payment.js",
"dependencies": {
"jquery": ">=1.5"
},
"homepage": "https://github.com/stripe/jquery.payment",
"version": "1.4.2",
"_release": "1.4.2",
"_resolution": {
"type": "version",
"tag": "v1.4.2",
"commit": "4269e038cd6f9b2c416d240a0e090c5ab0949c46"
},
"_source": "https://github.com/stripe/jquery.payment.git",
"_target": "^1.4.2",
"_originalSource": "https://github.com/stripe/jquery.payment.git",
"_direct": true
}

View File

@ -0,0 +1,16 @@
Thanks for your interest in contributing!
## Filing issues
`jQuery.payment` is not currently accepting feature requests. We _are_ interested in fixing bugs and updating credit card BINs where appropriate. Please file issues for these items only.
When filing bugs, make sure to follow the issue template. When reporting credit card BIN changes, please include links to supporting documentation.
## Submitting pull requests
We will happily review and merge pull requests for bugs and card number changes. When submitting a pull request:
- Ensure that the code and commit message are well-commented and grammatically correct.
- Follow the style of the rest of the code, where possible.
- Make sure that your diff is the minimal set of changes necessary to fix the bug in question.
- Follow the pull request template.

View File

@ -0,0 +1,11 @@
## Expected behavior
Describe the expected behavior. For new credit card BIN numbers, please include supporting references.
## Actual behavior
Describe the actual behavior. List all browsers affected, as specifically as possible (e.g. "Chrome 49.2623.112 on OS X 10.11.4", not just "Chrome").
## Steps to reproduce
Describe steps to reproduce the problem. Please include a link to a minimal reproduction of the problem with jsfiddle (or similar tool) where possible.

View File

@ -0,0 +1,11 @@
## Summary
Simple summary of what was changed.
## Motivation
Why are you making this change? Please provide supporting references for new BIN numbers and links to minimal reproductions (e.g. with jsfiddle) when fixing bugs.
## Testing
How was the code tested? Be as specific as possible.

View File

@ -0,0 +1 @@
node_modules

View File

@ -0,0 +1,3 @@
language: node_js
node_js:
- "4"

38
public/vendor/jquery.payment/Cakefile vendored Normal file
View File

@ -0,0 +1,38 @@
{spawn} = require 'child_process'
path = require 'path'
binPath = (bin) -> path.resolve(__dirname, "./node_modules/.bin/#{bin}")
runExternal = (cmd, args, callback = process.exit) ->
child = spawn(binPath(cmd), args, stdio: 'inherit')
child.on('error', console.error)
child.on('close', callback)
runSequential = (cmds, status = 0) ->
process.exit status if status or !cmds.length
cmd = cmds.shift()
cmd.push (status) -> runSequential cmds, status
runExternal.apply null, cmd
task 'build', 'Build lib/ from src/', ->
runExternal 'coffee',
['-c', '-o', 'lib', 'src'],
-> invoke 'minify'
task 'minify', 'Minify lib/', ->
runExternal 'uglifyjs', [
'lib/jquery.payment.js',
'--mangle',
'--compress',
'--output',
'lib/jquery.payment.min.js'
]
task 'watch', 'Watch src/ for changes', ->
runExternal 'coffee', ['-w', '-c', '-o', 'lib', 'src']
task 'test', 'Run tests', ->
runSequential [
['mocha', ['--compilers', 'coffee:coffee-script/register', 'test/jquery.coffee']]
['mocha', ['--compilers', 'coffee:coffee-script/register', 'test/zepto.coffee']]
]

20
public/vendor/jquery.payment/LICENSE vendored Normal file
View File

@ -0,0 +1,20 @@
Copyright (c) 2014 Stripe
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

252
public/vendor/jquery.payment/README.md vendored Normal file
View File

@ -0,0 +1,252 @@
# jQuery.payment [![Build Status](https://travis-ci.org/stripe/jquery.payment.svg?branch=master)](https://travis-ci.org/stripe/jquery.payment)
A general purpose library for building credit card forms, validating inputs and formatting numbers.
## Project status
We consider `jQuery.payment` to be feature complete. We continue to use it in production, and we will happily accept bug reports and pull requests fixing those bugs, but we will not be adding new features or modifying the project for new frameworks or build systems.
### Why?
The library was born in a different age, and we think it has served tremendously, but it is fundamentally doing too many things. Complecting DOM element manipulation, input masking, card formatting, and cursor positioning makes it difficult to test and modify. An ideal version of this library would separate the independent components and make the internal logic functional.
## Usage
You can make an input act like a credit card field (with number formatting and length restriction):
``` javascript
$('input.cc-num').payment('formatCardNumber');
```
Then, when the payment form is submitted, you can validate the card number on the client-side:
``` javascript
var valid = $.payment.validateCardNumber($('input.cc-num').val());
if (!valid) {
alert('Your card is not valid!');
return false;
}
```
You can find a full [demo here](http://stripe.github.io/jquery.payment/example).
Supported card types are:
* Visa
* MasterCard
* American Express
* Diners Club
* Discover
* UnionPay
* JCB
* Visa Electron
* Maestro
* Forbrugsforeningen
* Dankort
* Elo
(Additional card types are supported by extending the [`$.payment.cards`](#paymentcards) array.)
## API
### $.fn.payment('formatCardNumber')
Formats card numbers:
* Includes a space between every 4 digits
* Restricts input to numbers
* Limits to 16 numbers
* Supports American Express formatting
* Adds a class of the card type (e.g. 'visa') to the input
Example:
``` javascript
$('input.cc-num').payment('formatCardNumber');
```
### $.fn.payment('formatCardExpiry')
Formats card expiry:
* Includes a `/` between the month and year
* Restricts input to numbers
* Restricts length
Example:
``` javascript
$('input.cc-exp').payment('formatCardExpiry');
```
### $.fn.payment('formatCardCVC')
Formats card CVC:
* Restricts length to 4 numbers
* Restricts input to numbers
Example:
``` javascript
$('input.cc-cvc').payment('formatCardCVC');
```
### $.fn.payment('restrictNumeric')
General numeric input restriction.
Example:
``` javascript
$('[data-numeric]').payment('restrictNumeric');
```
### $.payment.validateCardNumber(number)
Validates a card number:
* Validates numbers
* Validates Luhn algorithm
* Validates length
Example:
``` javascript
$.payment.validateCardNumber('4242 4242 4242 4242'); //=> true
```
### $.payment.validateCardExpiry(month, year)
Validates a card expiry:
* Validates numbers
* Validates in the future
* Supports year shorthand
Example:
``` javascript
$.payment.validateCardExpiry('05', '20'); //=> true
$.payment.validateCardExpiry('05', '2015'); //=> true
$.payment.validateCardExpiry('05', '05'); //=> false
```
### $.payment.validateCardCVC(cvc, type)
Validates a card CVC:
* Validates number
* Validates length to 4
Example:
``` javascript
$.payment.validateCardCVC('123'); //=> true
$.payment.validateCardCVC('123', 'amex'); //=> true
$.payment.validateCardCVC('1234', 'amex'); //=> true
$.payment.validateCardCVC('12344'); //=> false
```
### $.payment.cardType(number)
Returns a card type. Either:
* `visa`
* `mastercard`
* `amex`
* `dinersclub`
* `discover`
* `unionpay`
* `jcb`
* `visaelectron`
* `maestro`
* `forbrugsforeningen`
* `dankort`
* `elo`
The function will return `null` if the card type can't be determined.
Example:
``` javascript
$.payment.cardType('4242 4242 4242 4242'); //=> 'visa'
```
### $.payment.cardExpiryVal(string) and $.fn.payment('cardExpiryVal')
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`).
``` javascript
$.payment.cardExpiryVal('03 / 2025'); //=> {month: 3, year: 2025}
$.payment.cardExpiryVal('05 / 04'); //=> {month: 5, year: 2004}
$('input.cc-exp').payment('cardExpiryVal') //=> {month: 4, year: 2020}
```
This function doesn't perform any validation of the month or year; use `$.payment.validateCardExpiry(month, year)` for that.
### $.payment.cards
Array of objects that describe valid card types. Each object should contain the following fields:
``` javascript
{
// Card type, as returned by $.payment.cardType.
type: 'mastercard',
// Array of prefixes used to identify the card type.
patterns: [
51, 52, 53, 54, 55,
22, 23, 24, 25, 26, 27
],
// Array of valid card number lengths.
length: [16],
// Array of valid card CVC lengths.
cvcLength: [3],
// Boolean indicating whether a valid card number should satisfy the Luhn check.
luhn: true,
// Regex used to format the card number. Each match is joined with a space.
format: /(\d{1,4})/g
}
```
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.
## Example
Look in [`./example/index.html`](example/index.html)
## Building
Run `cake build`
## Running tests
Run `cake test`
## Autocomplete recommendations
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:
``` html
<form autocomplete="on">
<input class="cc-number">
<input class="cc-cvc" autocomplete="off">
</form>
```
You should also 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.
``` html
<input type="tel" class="cc-number" autocomplete="cc-number">
```
Set `autocomplete` to `cc-number` for credit card numbers and `cc-exp` for credit card expiry.
## Mobile recommendations
We recommend you to use `<input type="tel">` which will cause the numeric keyboard to be displayed on mobile devices:
``` html
<input type="tel" class="cc-number">
```

View File

@ -0,0 +1,7 @@
{
"name": "jquery.payment",
"main": "lib/jquery.payment.js",
"dependencies": {
"jquery": ">=1.5"
}
}

View File

@ -0,0 +1,86 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.3/jquery.min.js"></script>
<script src="../lib/jquery.payment.js"></script>
<style type="text/css" media="screen">
.has-error input {
border-width: 2px;
}
.validation.text-danger:after {
content: 'Validation failed';
}
.validation.text-success:after {
content: 'Validation passed';
}
</style>
<script>
jQuery(function($) {
$('[data-numeric]').payment('restrictNumeric');
$('.cc-number').payment('formatCardNumber');
$('.cc-exp').payment('formatCardExpiry');
$('.cc-cvc').payment('formatCardCVC');
$.fn.toggleInputError = function(erred) {
this.parent('.form-group').toggleClass('has-error', erred);
return this;
};
$('form').submit(function(e) {
e.preventDefault();
var cardType = $.payment.cardType($('.cc-number').val());
$('.cc-number').toggleInputError(!$.payment.validateCardNumber($('.cc-number').val()));
$('.cc-exp').toggleInputError(!$.payment.validateCardExpiry($('.cc-exp').payment('cardExpiryVal')));
$('.cc-cvc').toggleInputError(!$.payment.validateCardCVC($('.cc-cvc').val(), cardType));
$('.cc-brand').text(cardType);
$('.validation').removeClass('text-danger text-success');
$('.validation').addClass($('.has-error').length ? 'text-danger' : 'text-success');
});
});
</script>
</head>
<body>
<div class="container">
<h1>
jquery.payment demo
<small><a class="btn btn-info btn-xs" href="https://github.com/stripe/jquery.payment">Fork on GitHub</a></small>
</h1>
<p>A general purpose library for building credit card forms, validating inputs and formatting numbers.</p>
<form novalidate autocomplete="on" method="POST">
<div class="form-group">
<label for="cc-number" class="control-label">Card number formatting <small class="text-muted">[<span class="cc-brand"></span>]</small></label>
<input id="cc-number" type="tel" class="input-lg form-control cc-number" autocomplete="cc-number" placeholder="•••• •••• •••• ••••" required>
</div>
<div class="form-group">
<label for="cc-exp" class="control-label">Card expiry formatting</label>
<input id="cc-exp" type="tel" class="input-lg form-control cc-exp" autocomplete="cc-exp" placeholder="•• / ••" required>
</div>
<div class="form-group">
<label for="cc-cvc" class="control-label">Card CVC formatting</label>
<input id="cc-cvc" type="tel" class="input-lg form-control cc-cvc" autocomplete="off" placeholder="•••" required>
</div>
<div class="form-group">
<label for="numeric" class="control-label">Restrict numeric</label>
<input id="numeric" type="tel" class="input-lg form-control" data-numeric>
</div>
<button type="submit" class="btn btn-lg btn-primary">Submit</button>
<h2 class="validation"></h2>
</form>
</div>
</body>
</html>

View File

@ -0,0 +1,666 @@
// Generated by CoffeeScript 1.7.1
(function() {
var $, cardFromNumber, cardFromType, cards, defaultFormat, formatBackCardNumber, formatBackExpiry, formatCardNumber, formatExpiry, formatForwardExpiry, formatForwardSlashAndSpace, hasTextSelected, luhnCheck, reFormatCVC, reFormatCardNumber, reFormatExpiry, reFormatNumeric, replaceFullWidthChars, restrictCVC, restrictCardNumber, restrictExpiry, restrictNumeric, safeVal, setCardType,
__slice = [].slice,
__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; };
$ = window.jQuery || window.Zepto || window.$;
$.payment = {};
$.payment.fn = {};
$.fn.payment = function() {
var args, method;
method = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
return $.payment.fn[method].apply(this, args);
};
defaultFormat = /(\d{1,4})/g;
$.payment.cards = cards = [
{
type: 'elo',
patterns: [4011, 4312, 4389, 4514, 4573, 4576, 5041, 5066, 5067, 509, 6277, 6362, 6363, 650, 6516, 6550],
format: defaultFormat,
length: [16],
cvcLength: [3],
luhn: true
}, {
type: 'visaelectron',
patterns: [4026, 417500, 4405, 4508, 4844, 4913, 4917],
format: defaultFormat,
length: [16],
cvcLength: [3],
luhn: true
}, {
type: 'maestro',
patterns: [5018, 502, 503, 506, 56, 58, 639, 6220, 67],
format: defaultFormat,
length: [12, 13, 14, 15, 16, 17, 18, 19],
cvcLength: [3],
luhn: true
}, {
type: 'forbrugsforeningen',
patterns: [600],
format: defaultFormat,
length: [16],
cvcLength: [3],
luhn: true
}, {
type: 'dankort',
patterns: [5019],
format: defaultFormat,
length: [16],
cvcLength: [3],
luhn: true
}, {
type: 'visa',
patterns: [4],
format: defaultFormat,
length: [13, 16],
cvcLength: [3],
luhn: true
}, {
type: 'mastercard',
patterns: [51, 52, 53, 54, 55, 22, 23, 24, 25, 26, 27],
format: defaultFormat,
length: [16],
cvcLength: [3],
luhn: true
}, {
type: 'amex',
patterns: [34, 37],
format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/,
length: [15],
cvcLength: [3, 4],
luhn: true
}, {
type: 'dinersclub',
patterns: [30, 36, 38, 39],
format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/,
length: [14],
cvcLength: [3],
luhn: true
}, {
type: 'discover',
patterns: [60, 64, 65, 622],
format: defaultFormat,
length: [16],
cvcLength: [3],
luhn: true
}, {
type: 'unionpay',
patterns: [62, 88],
format: defaultFormat,
length: [16, 17, 18, 19],
cvcLength: [3],
luhn: false
}, {
type: 'jcb',
patterns: [35],
format: defaultFormat,
length: [16],
cvcLength: [3],
luhn: true
}
];
cardFromNumber = function(num) {
var card, p, pattern, _i, _j, _len, _len1, _ref;
num = (num + '').replace(/\D/g, '');
for (_i = 0, _len = cards.length; _i < _len; _i++) {
card = cards[_i];
_ref = card.patterns;
for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) {
pattern = _ref[_j];
p = pattern + '';
if (num.substr(0, p.length) === p) {
return card;
}
}
}
};
cardFromType = function(type) {
var card, _i, _len;
for (_i = 0, _len = cards.length; _i < _len; _i++) {
card = cards[_i];
if (card.type === type) {
return card;
}
}
};
luhnCheck = function(num) {
var digit, digits, odd, sum, _i, _len;
odd = true;
sum = 0;
digits = (num + '').split('').reverse();
for (_i = 0, _len = digits.length; _i < _len; _i++) {
digit = digits[_i];
digit = parseInt(digit, 10);
if ((odd = !odd)) {
digit *= 2;
}
if (digit > 9) {
digit -= 9;
}
sum += digit;
}
return sum % 10 === 0;
};
hasTextSelected = function($target) {
var _ref;
if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== $target.prop('selectionEnd')) {
return true;
}
if ((typeof document !== "undefined" && document !== null ? (_ref = document.selection) != null ? _ref.createRange : void 0 : void 0) != null) {
if (document.selection.createRange().text) {
return true;
}
}
return false;
};
safeVal = function(value, $target) {
var currPair, cursor, digit, error, last, prevPair;
try {
cursor = $target.prop('selectionStart');
} catch (_error) {
error = _error;
cursor = null;
}
last = $target.val();
$target.val(value);
if (cursor !== null && $target.is(":focus")) {
if (cursor === last.length) {
cursor = value.length;
}
if (last !== value) {
prevPair = last.slice(cursor - 1, +cursor + 1 || 9e9);
currPair = value.slice(cursor - 1, +cursor + 1 || 9e9);
digit = value[cursor];
if (/\d/.test(digit) && prevPair === ("" + digit + " ") && currPair === (" " + digit)) {
cursor = cursor + 1;
}
}
$target.prop('selectionStart', cursor);
return $target.prop('selectionEnd', cursor);
}
};
replaceFullWidthChars = function(str) {
var chars, chr, fullWidth, halfWidth, idx, value, _i, _len;
if (str == null) {
str = '';
}
fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19';
halfWidth = '0123456789';
value = '';
chars = str.split('');
for (_i = 0, _len = chars.length; _i < _len; _i++) {
chr = chars[_i];
idx = fullWidth.indexOf(chr);
if (idx > -1) {
chr = halfWidth[idx];
}
value += chr;
}
return value;
};
reFormatNumeric = function(e) {
var $target;
$target = $(e.currentTarget);
return setTimeout(function() {
var value;
value = $target.val();
value = replaceFullWidthChars(value);
value = value.replace(/\D/g, '');
return safeVal(value, $target);
});
};
reFormatCardNumber = function(e) {
var $target;
$target = $(e.currentTarget);
return setTimeout(function() {
var value;
value = $target.val();
value = replaceFullWidthChars(value);
value = $.payment.formatCardNumber(value);
return safeVal(value, $target);
});
};
formatCardNumber = function(e) {
var $target, card, digit, length, re, upperLength, value;
digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) {
return;
}
$target = $(e.currentTarget);
value = $target.val();
card = cardFromNumber(value + digit);
length = (value.replace(/\D/g, '') + digit).length;
upperLength = 16;
if (card) {
upperLength = card.length[card.length.length - 1];
}
if (length >= upperLength) {
return;
}
if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
return;
}
if (card && card.type === 'amex') {
re = /^(\d{4}|\d{4}\s\d{6})$/;
} else {
re = /(?:^|\s)(\d{4})$/;
}
if (re.test(value)) {
e.preventDefault();
return setTimeout(function() {
return $target.val(value + ' ' + digit);
});
} else if (re.test(value + digit)) {
e.preventDefault();
return setTimeout(function() {
return $target.val(value + digit + ' ');
});
}
};
formatBackCardNumber = function(e) {
var $target, value;
$target = $(e.currentTarget);
value = $target.val();
if (e.which !== 8) {
return;
}
if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
return;
}
if (/\d\s$/.test(value)) {
e.preventDefault();
return setTimeout(function() {
return $target.val(value.replace(/\d\s$/, ''));
});
} else if (/\s\d?$/.test(value)) {
e.preventDefault();
return setTimeout(function() {
return $target.val(value.replace(/\d$/, ''));
});
}
};
reFormatExpiry = function(e) {
var $target;
$target = $(e.currentTarget);
return setTimeout(function() {
var value;
value = $target.val();
value = replaceFullWidthChars(value);
value = $.payment.formatExpiry(value);
return safeVal(value, $target);
});
};
formatExpiry = function(e) {
var $target, digit, val;
digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) {
return;
}
$target = $(e.currentTarget);
val = $target.val() + digit;
if (/^\d$/.test(val) && (val !== '0' && val !== '1')) {
e.preventDefault();
return setTimeout(function() {
return $target.val("0" + val + " / ");
});
} else if (/^\d\d$/.test(val)) {
e.preventDefault();
return setTimeout(function() {
var m1, m2;
m1 = parseInt(val[0], 10);
m2 = parseInt(val[1], 10);
if (m2 > 2 && m1 !== 0) {
return $target.val("0" + m1 + " / " + m2);
} else {
return $target.val("" + val + " / ");
}
});
}
};
formatForwardExpiry = function(e) {
var $target, digit, val;
digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) {
return;
}
$target = $(e.currentTarget);
val = $target.val();
if (/^\d\d$/.test(val)) {
return $target.val("" + val + " / ");
}
};
formatForwardSlashAndSpace = function(e) {
var $target, val, which;
which = String.fromCharCode(e.which);
if (!(which === '/' || which === ' ')) {
return;
}
$target = $(e.currentTarget);
val = $target.val();
if (/^\d$/.test(val) && val !== '0') {
return $target.val("0" + val + " / ");
}
};
formatBackExpiry = function(e) {
var $target, value;
$target = $(e.currentTarget);
value = $target.val();
if (e.which !== 8) {
return;
}
if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
return;
}
if (/\d\s\/\s$/.test(value)) {
e.preventDefault();
return setTimeout(function() {
return $target.val(value.replace(/\d\s\/\s$/, ''));
});
}
};
reFormatCVC = function(e) {
var $target;
$target = $(e.currentTarget);
return setTimeout(function() {
var value;
value = $target.val();
value = replaceFullWidthChars(value);
value = value.replace(/\D/g, '').slice(0, 4);
return safeVal(value, $target);
});
};
restrictNumeric = function(e) {
var input;
if (e.metaKey || e.ctrlKey) {
return true;
}
if (e.which === 32) {
return false;
}
if (e.which === 0) {
return true;
}
if (e.which < 33) {
return true;
}
input = String.fromCharCode(e.which);
return !!/[\d\s]/.test(input);
};
restrictCardNumber = function(e) {
var $target, card, digit, value;
$target = $(e.currentTarget);
digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) {
return;
}
if (hasTextSelected($target)) {
return;
}
value = ($target.val() + digit).replace(/\D/g, '');
card = cardFromNumber(value);
if (card) {
return value.length <= card.length[card.length.length - 1];
} else {
return value.length <= 16;
}
};
restrictExpiry = function(e) {
var $target, digit, value;
$target = $(e.currentTarget);
digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) {
return;
}
if (hasTextSelected($target)) {
return;
}
value = $target.val() + digit;
value = value.replace(/\D/g, '');
if (value.length > 6) {
return false;
}
};
restrictCVC = function(e) {
var $target, digit, val;
$target = $(e.currentTarget);
digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) {
return;
}
if (hasTextSelected($target)) {
return;
}
val = $target.val() + digit;
return val.length <= 4;
};
setCardType = function(e) {
var $target, allTypes, card, cardType, val;
$target = $(e.currentTarget);
val = $target.val();
cardType = $.payment.cardType(val) || 'unknown';
if (!$target.hasClass(cardType)) {
allTypes = (function() {
var _i, _len, _results;
_results = [];
for (_i = 0, _len = cards.length; _i < _len; _i++) {
card = cards[_i];
_results.push(card.type);
}
return _results;
})();
$target.removeClass('unknown');
$target.removeClass(allTypes.join(' '));
$target.addClass(cardType);
$target.toggleClass('identified', cardType !== 'unknown');
return $target.trigger('payment.cardType', cardType);
}
};
$.payment.fn.formatCardCVC = function() {
this.on('keypress', restrictNumeric);
this.on('keypress', restrictCVC);
this.on('paste', reFormatCVC);
this.on('change', reFormatCVC);
this.on('input', reFormatCVC);
return this;
};
$.payment.fn.formatCardExpiry = function() {
this.on('keypress', restrictNumeric);
this.on('keypress', restrictExpiry);
this.on('keypress', formatExpiry);
this.on('keypress', formatForwardSlashAndSpace);
this.on('keypress', formatForwardExpiry);
this.on('keydown', formatBackExpiry);
this.on('change', reFormatExpiry);
this.on('input', reFormatExpiry);
return this;
};
$.payment.fn.formatCardNumber = function() {
this.on('keypress', restrictNumeric);
this.on('keypress', restrictCardNumber);
this.on('keypress', formatCardNumber);
this.on('keydown', formatBackCardNumber);
this.on('keyup', setCardType);
this.on('paste', reFormatCardNumber);
this.on('change', reFormatCardNumber);
this.on('input', reFormatCardNumber);
this.on('input', setCardType);
return this;
};
$.payment.fn.restrictNumeric = function() {
this.on('keypress', restrictNumeric);
this.on('paste', reFormatNumeric);
this.on('change', reFormatNumeric);
this.on('input', reFormatNumeric);
return this;
};
$.payment.fn.cardExpiryVal = function() {
return $.payment.cardExpiryVal($(this).val());
};
$.payment.cardExpiryVal = function(value) {
var month, prefix, year, _ref;
_ref = value.split(/[\s\/]+/, 2), month = _ref[0], year = _ref[1];
if ((year != null ? year.length : void 0) === 2 && /^\d+$/.test(year)) {
prefix = (new Date).getFullYear();
prefix = prefix.toString().slice(0, 2);
year = prefix + year;
}
month = parseInt(month, 10);
year = parseInt(year, 10);
return {
month: month,
year: year
};
};
$.payment.validateCardNumber = function(num) {
var card, _ref;
num = (num + '').replace(/\s+|-/g, '');
if (!/^\d+$/.test(num)) {
return false;
}
card = cardFromNumber(num);
if (!card) {
return false;
}
return (_ref = num.length, __indexOf.call(card.length, _ref) >= 0) && (card.luhn === false || luhnCheck(num));
};
$.payment.validateCardExpiry = function(month, year) {
var currentTime, expiry, _ref;
if (typeof month === 'object' && 'month' in month) {
_ref = month, month = _ref.month, year = _ref.year;
}
if (!(month && year)) {
return false;
}
month = $.trim(month);
year = $.trim(year);
if (!/^\d+$/.test(month)) {
return false;
}
if (!/^\d+$/.test(year)) {
return false;
}
if (!((1 <= month && month <= 12))) {
return false;
}
if (year.length === 2) {
if (year < 70) {
year = "20" + year;
} else {
year = "19" + year;
}
}
if (year.length !== 4) {
return false;
}
expiry = new Date(year, month);
currentTime = new Date;
expiry.setMonth(expiry.getMonth() - 1);
expiry.setMonth(expiry.getMonth() + 1, 1);
return expiry > currentTime;
};
$.payment.validateCardCVC = function(cvc, type) {
var card, _ref;
cvc = $.trim(cvc);
if (!/^\d+$/.test(cvc)) {
return false;
}
card = cardFromType(type);
if (card != null) {
return _ref = cvc.length, __indexOf.call(card.cvcLength, _ref) >= 0;
} else {
return cvc.length >= 3 && cvc.length <= 4;
}
};
$.payment.cardType = function(num) {
var _ref;
if (!num) {
return null;
}
return ((_ref = cardFromNumber(num)) != null ? _ref.type : void 0) || null;
};
$.payment.formatCardNumber = function(num) {
var card, groups, upperLength, _ref;
num = num.replace(/\D/g, '');
card = cardFromNumber(num);
if (!card) {
return num;
}
upperLength = card.length[card.length.length - 1];
num = num.slice(0, upperLength);
if (card.format.global) {
return (_ref = num.match(card.format)) != null ? _ref.join(' ') : void 0;
} else {
groups = card.format.exec(num);
if (groups == null) {
return;
}
groups.shift();
groups = $.grep(groups, function(n) {
return n;
});
return groups.join(' ');
}
};
$.payment.formatExpiry = function(expiry) {
var mon, parts, sep, year;
parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/);
if (!parts) {
return '';
}
mon = parts[1] || '';
sep = parts[2] || '';
year = parts[3] || '';
if (year.length > 0) {
sep = ' / ';
} else if (sep === ' /') {
mon = mon.substring(0, 1);
sep = '';
} else if (mon.length === 2 || sep.length > 0) {
sep = ' / ';
} else if (mon.length === 1 && (mon !== '0' && mon !== '1')) {
mon = "0" + mon;
sep = ' / ';
}
return mon + sep + year;
};
}).call(this);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,31 @@
{
"name": "jquery.payment",
"version": "1.4.2",
"description": "A general purpose library for building credit card forms, validating inputs and formatting numbers.",
"keywords": [
"payment",
"cc",
"card"
],
"author": "Stripe (https://www.stripe.com)",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/stripe/jquery.payment.git"
},
"main": "lib/jquery.payment.js",
"scripts": {
"test": "cake test"
},
"dependencies": {
"jquery": ">=1.7"
},
"devDependencies": {
"cake": "~0.1",
"coffee-script": "~1.7",
"jsdom": "~7.2",
"mocha": "~1.18",
"uglify-js": "~2.4.24",
"zeptojs": "~1.1"
}
}

View File

@ -0,0 +1,29 @@
{
"name": "payment",
"version": "1.4.2",
"title": "jQuery.payment",
"description": "A general purpose library for building credit card forms, validating inputs and formatting numbers.",
"keywords": [
"payment",
"cc",
"card"
],
"author": {
"name": "Stripe",
"url": "https://www.stripe.com",
"email": "support+github@stripe.com"
},
"licenses": [
{
"type": "MIT",
"url": "https://github.com/stripe/jquery.payment/blob/master/LICENSE"
}
],
"homepage": "https://github.com/stripe/jquery.payment",
"docs": "https://github.com/stripe/jquery.payment",
"bugs": "https://github.com/stripe/jquery.payment/issues",
"demo": "http://stripe.github.io/jquery.payment/example",
"dependencies": {
"jquery": ">=1.7"
}
}

View File

@ -0,0 +1,608 @@
$ = window.jQuery or window.Zepto or window.$
$.payment = {}
$.payment.fn = {}
$.fn.payment = (method, args...) ->
$.payment.fn[method].apply(this, args)
# Utils
defaultFormat = /(\d{1,4})/g
$.payment.cards = cards = [
# Specifics patterns must be analysed first or it
# will generate false positives
{
type: 'elo'
patterns: [
4011, 4312, 4389, 4514, 4573, 4576,
5041, 5066, 5067, 509,
6277, 6362, 6363, 650, 6516, 6550
]
format: defaultFormat
length: [16]
cvcLength: [3]
luhn: true
}
{
type: 'visaelectron'
patterns: [
4026, 417500, 4405, 4508, 4844, 4913, 4917
]
format: defaultFormat
length: [16]
cvcLength: [3]
luhn: true
}
{
type: 'maestro'
patterns: [
5018, 502, 503, 506, 56, 58, 639, 6220, 67
]
format: defaultFormat
length: [12..19]
cvcLength: [3]
luhn: true
}
{
type: 'forbrugsforeningen'
patterns: [600]
format: defaultFormat
length: [16]
cvcLength: [3]
luhn: true
}
{
type: 'dankort'
patterns: [5019]
format: defaultFormat
length: [16]
cvcLength: [3]
luhn: true
}
# Credit cards
{
type: 'visa'
patterns: [4]
format: defaultFormat
length: [13, 16]
cvcLength: [3]
luhn: true
}
{
type: 'mastercard'
patterns: [
51, 52, 53, 54, 55,
22, 23, 24, 25, 26, 27
]
format: defaultFormat
length: [16]
cvcLength: [3]
luhn: true
}
{
type: 'amex'
patterns: [34, 37]
format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/
length: [15]
cvcLength: [3..4]
luhn: true
}
{
type: 'dinersclub'
patterns: [30, 36, 38, 39]
format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/
length: [14]
cvcLength: [3]
luhn: true
}
{
type: 'discover'
patterns: [60, 64, 65, 622]
format: defaultFormat
length: [16]
cvcLength: [3]
luhn: true
}
{
type: 'unionpay'
patterns: [62, 88]
format: defaultFormat
length: [16..19]
cvcLength: [3]
luhn: false
}
{
type: 'jcb'
patterns: [35]
format: defaultFormat
length: [16]
cvcLength: [3]
luhn: true
}
]
cardFromNumber = (num) ->
num = (num + '').replace(/\D/g, '')
for card in cards
for pattern in card.patterns
p = pattern + ''
return card if num.substr(0, p.length) == p
cardFromType = (type) ->
return card for card in cards when card.type is type
luhnCheck = (num) ->
odd = true
sum = 0
digits = (num + '').split('').reverse()
for digit in digits
digit = parseInt(digit, 10)
digit *= 2 if (odd = !odd)
digit -= 9 if digit > 9
sum += digit
sum % 10 == 0
hasTextSelected = ($target) ->
# If some text is selected
return true if $target.prop('selectionStart')? and
$target.prop('selectionStart') isnt $target.prop('selectionEnd')
# If some text is selected in IE
if document?.selection?.createRange?
return true if document.selection.createRange().text
false
# Private
# Safe Val
safeVal = (value, $target) ->
try
cursor = $target.prop('selectionStart')
catch error
cursor = null
last = $target.val()
$target.val(value)
if cursor != null && $target.is(":focus")
cursor = value.length if cursor is last.length
# This hack looks for scenarios where we are changing an input's value such
# that "X| " is replaced with " |X" (where "|" is the cursor). In those
# scenarios, we want " X|".
#
# For example:
# 1. Input field has value "4444| "
# 2. User types "1"
# 3. Input field has value "44441| "
# 4. Reformatter changes it to "4444 |1"
# 5. By incrementing the cursor, we make it "4444 1|"
#
# This is awful, and ideally doesn't go here, but given the current design
# of the system there does not appear to be a better solution.
#
# Note that we can't just detect when the cursor-1 is " ", because that
# would incorrectly increment the cursor when backspacing, e.g. pressing
# backspace in this scenario: "4444 1|234 5".
if last != value
prevPair = last[cursor-1..cursor]
currPair = value[cursor-1..cursor]
digit = value[cursor]
cursor = cursor + 1 if /\d/.test(digit) and
prevPair == "#{digit} " and currPair == " #{digit}"
$target.prop('selectionStart', cursor)
$target.prop('selectionEnd', cursor)
# Replace Full-Width Chars
replaceFullWidthChars = (str = '') ->
fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19'
halfWidth = '0123456789'
value = ''
chars = str.split('')
# Avoid using reserved word `char`
for chr in chars
idx = fullWidth.indexOf(chr)
chr = halfWidth[idx] if idx > -1
value += chr
value
# Format Numeric
reFormatNumeric = (e) ->
$target = $(e.currentTarget)
setTimeout ->
value = $target.val()
value = replaceFullWidthChars(value)
value = value.replace(/\D/g, '')
safeVal(value, $target)
# Format Card Number
reFormatCardNumber = (e) ->
$target = $(e.currentTarget)
setTimeout ->
value = $target.val()
value = replaceFullWidthChars(value)
value = $.payment.formatCardNumber(value)
safeVal(value, $target)
formatCardNumber = (e) ->
# Only format if input is a number
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
$target = $(e.currentTarget)
value = $target.val()
card = cardFromNumber(value + digit)
length = (value.replace(/\D/g, '') + digit).length
upperLength = 16
upperLength = card.length[card.length.length - 1] if card
return if length >= upperLength
# Return if focus isn't at the end of the text
return if $target.prop('selectionStart')? and
$target.prop('selectionStart') isnt value.length
if card && card.type is 'amex'
# AMEX cards are formatted differently
re = /^(\d{4}|\d{4}\s\d{6})$/
else
re = /(?:^|\s)(\d{4})$/
# If '4242' + 4
if re.test(value)
e.preventDefault()
setTimeout -> $target.val(value + ' ' + digit)
# If '424' + 2
else if re.test(value + digit)
e.preventDefault()
setTimeout -> $target.val(value + digit + ' ')
formatBackCardNumber = (e) ->
$target = $(e.currentTarget)
value = $target.val()
# Return unless backspacing
return unless e.which is 8
# Return if focus isn't at the end of the text
return if $target.prop('selectionStart')? and
$target.prop('selectionStart') isnt value.length
# Remove the digit + trailing space
if /\d\s$/.test(value)
e.preventDefault()
setTimeout -> $target.val(value.replace(/\d\s$/, ''))
# Remove digit if ends in space + digit
else if /\s\d?$/.test(value)
e.preventDefault()
setTimeout -> $target.val(value.replace(/\d$/, ''))
# Format Expiry
reFormatExpiry = (e) ->
$target = $(e.currentTarget)
setTimeout ->
value = $target.val()
value = replaceFullWidthChars(value)
value = $.payment.formatExpiry(value)
safeVal(value, $target)
formatExpiry = (e) ->
# Only format if input is a number
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
$target = $(e.currentTarget)
val = $target.val() + digit
if /^\d$/.test(val) and val not in ['0', '1']
e.preventDefault()
setTimeout -> $target.val("0#{val} / ")
else if /^\d\d$/.test(val)
e.preventDefault()
setTimeout ->
# Split for months where we have the second digit > 2 (past 12) and turn
# that into (m1)(m2) => 0(m1) / (m2)
m1 = parseInt(val[0], 10)
m2 = parseInt(val[1], 10)
if m2 > 2 and m1 != 0
$target.val("0#{m1} / #{m2}")
else
$target.val("#{val} / ")
formatForwardExpiry = (e) ->
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
$target = $(e.currentTarget)
val = $target.val()
if /^\d\d$/.test(val)
$target.val("#{val} / ")
formatForwardSlashAndSpace = (e) ->
which = String.fromCharCode(e.which)
return unless which is '/' or which is ' '
$target = $(e.currentTarget)
val = $target.val()
if /^\d$/.test(val) and val isnt '0'
$target.val("0#{val} / ")
formatBackExpiry = (e) ->
$target = $(e.currentTarget)
value = $target.val()
# Return unless backspacing
return unless e.which is 8
# Return if focus isn't at the end of the text
return if $target.prop('selectionStart')? and
$target.prop('selectionStart') isnt value.length
# Remove the trailing space + last digit
if /\d\s\/\s$/.test(value)
e.preventDefault()
setTimeout -> $target.val(value.replace(/\d\s\/\s$/, ''))
# Format CVC
reFormatCVC = (e) ->
$target = $(e.currentTarget)
setTimeout ->
value = $target.val()
value = replaceFullWidthChars(value)
value = value.replace(/\D/g, '')[0...4]
safeVal(value, $target)
# Restrictions
restrictNumeric = (e) ->
# Key event is for a browser shortcut
return true if e.metaKey or e.ctrlKey
# If keycode is a space
return false if e.which is 32
# If keycode is a special char (WebKit)
return true if e.which is 0
# If char is a special char (Firefox)
return true if e.which < 33
input = String.fromCharCode(e.which)
# Char is a number or a space
!!/[\d\s]/.test(input)
restrictCardNumber = (e) ->
$target = $(e.currentTarget)
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
return if hasTextSelected($target)
# Restrict number of digits
value = ($target.val() + digit).replace(/\D/g, '')
card = cardFromNumber(value)
if card
value.length <= card.length[card.length.length - 1]
else
# All other cards are 16 digits long
value.length <= 16
restrictExpiry = (e) ->
$target = $(e.currentTarget)
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
return if hasTextSelected($target)
value = $target.val() + digit
value = value.replace(/\D/g, '')
return false if value.length > 6
restrictCVC = (e) ->
$target = $(e.currentTarget)
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
return if hasTextSelected($target)
val = $target.val() + digit
val.length <= 4
setCardType = (e) ->
$target = $(e.currentTarget)
val = $target.val()
cardType = $.payment.cardType(val) or 'unknown'
unless $target.hasClass(cardType)
allTypes = (card.type for card in cards)
$target.removeClass('unknown')
$target.removeClass(allTypes.join(' '))
$target.addClass(cardType)
$target.toggleClass('identified', cardType isnt 'unknown')
$target.trigger('payment.cardType', cardType)
# Public
# Formatting
$.payment.fn.formatCardCVC = ->
@on('keypress', restrictNumeric)
@on('keypress', restrictCVC)
@on('paste', reFormatCVC)
@on('change', reFormatCVC)
@on('input', reFormatCVC)
this
$.payment.fn.formatCardExpiry = ->
@on('keypress', restrictNumeric)
@on('keypress', restrictExpiry)
@on('keypress', formatExpiry)
@on('keypress', formatForwardSlashAndSpace)
@on('keypress', formatForwardExpiry)
@on('keydown', formatBackExpiry)
@on('change', reFormatExpiry)
@on('input', reFormatExpiry)
this
$.payment.fn.formatCardNumber = ->
@on('keypress', restrictNumeric)
@on('keypress', restrictCardNumber)
@on('keypress', formatCardNumber)
@on('keydown', formatBackCardNumber)
@on('keyup', setCardType)
@on('paste', reFormatCardNumber)
@on('change', reFormatCardNumber)
@on('input', reFormatCardNumber)
@on('input', setCardType)
this
# Restrictions
$.payment.fn.restrictNumeric = ->
@on('keypress', restrictNumeric)
@on('paste', reFormatNumeric)
@on('change', reFormatNumeric)
@on('input', reFormatNumeric)
this
# Validations
$.payment.fn.cardExpiryVal = ->
$.payment.cardExpiryVal($(this).val())
$.payment.cardExpiryVal = (value) ->
[month, year] = value.split(/[\s\/]+/, 2)
# Allow for year shortcut
if year?.length is 2 and /^\d+$/.test(year)
prefix = (new Date).getFullYear()
prefix = prefix.toString()[0..1]
year = prefix + year
month = parseInt(month, 10)
year = parseInt(year, 10)
month: month, year: year
$.payment.validateCardNumber = (num) ->
num = (num + '').replace(/\s+|-/g, '')
return false unless /^\d+$/.test(num)
card = cardFromNumber(num)
return false unless card
num.length in card.length and
(card.luhn is false or luhnCheck(num))
$.payment.validateCardExpiry = (month, year) ->
# Allow passing an object
if typeof month is 'object' and 'month' of month
{month, year} = month
return false unless month and year
month = $.trim(month)
year = $.trim(year)
return false unless /^\d+$/.test(month)
return false unless /^\d+$/.test(year)
return false unless 1 <= month <= 12
if year.length == 2
if year < 70
year = "20#{year}"
else
year = "19#{year}"
return false unless year.length == 4
expiry = new Date(year, month)
currentTime = new Date
# Months start from 0 in JavaScript
expiry.setMonth(expiry.getMonth() - 1)
# The cc expires at the end of the month,
# so we need to make the expiry the first day
# of the month after
expiry.setMonth(expiry.getMonth() + 1, 1)
expiry > currentTime
$.payment.validateCardCVC = (cvc, type) ->
cvc = $.trim(cvc)
return false unless /^\d+$/.test(cvc)
card = cardFromType(type)
if card?
# Check against a explicit card type
cvc.length in card.cvcLength
else
# Check against all types
cvc.length >= 3 and cvc.length <= 4
$.payment.cardType = (num) ->
return null unless num
cardFromNumber(num)?.type or null
$.payment.formatCardNumber = (num) ->
num = num.replace(/\D/g, '')
card = cardFromNumber(num)
return num unless card
upperLength = card.length[card.length.length - 1]
num = num[0...upperLength]
if card.format.global
num.match(card.format)?.join(' ')
else
groups = card.format.exec(num)
return unless groups?
groups.shift()
groups = $.grep(groups, (n) -> n) # Filter empty groups
groups.join(' ')
$.payment.formatExpiry = (expiry) ->
parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/)
return '' unless parts
mon = parts[1] || ''
sep = parts[2] || ''
year = parts[3] || ''
if year.length > 0
sep = ' / '
else if sep is ' /'
mon = mon.substring(0, 1)
sep = ''
else if mon.length == 2 or sep.length > 0
sep = ' / '
else if mon.length == 1 and mon not in ['0', '1']
mon = "0#{mon}"
sep = ' / '
return mon + sep + year

View File

@ -0,0 +1,7 @@
window = require('jsdom').jsdom().defaultView
global.$ = require('jquery')(window)
global.window = window
global.document = window.document
require('../src/jquery.payment')
require('./specs')

View File

@ -0,0 +1,407 @@
assert = require('assert')
describe 'jquery.payment', ->
describe 'Validating a card number', ->
it 'should fail if empty', ->
topic = $.payment.validateCardNumber ''
assert.equal topic, false
it 'should fail if is a bunch of spaces', ->
topic = $.payment.validateCardNumber ' '
assert.equal topic, false
it 'should success if is valid', ->
topic = $.payment.validateCardNumber '4242424242424242'
assert.equal topic, true
it 'that has dashes in it but is valid', ->
topic = $.payment.validateCardNumber '4242-4242-4242-4242'
assert.equal topic, true
it 'should succeed if it has spaces in it but is valid', ->
topic = $.payment.validateCardNumber '4242 4242 4242 4242'
assert.equal topic, true
it 'that does not pass the luhn checker', ->
topic = $.payment.validateCardNumber '4242424242424241'
assert.equal topic, false
it 'should fail if is more than 16 digits', ->
topic = $.payment.validateCardNumber '42424242424242424'
assert.equal topic, false
it 'should fail if is less than 10 digits', ->
topic = $.payment.validateCardNumber '424242424'
assert.equal topic, false
it 'should fail with non-digits', ->
topic = $.payment.validateCardNumber '4242424e42424241'
assert.equal topic, false
it 'should validate for all card types', ->
assert($.payment.validateCardNumber('4917300800000000'), 'visaelectron')
assert($.payment.validateCardNumber('6759649826438453'), 'maestro')
assert($.payment.validateCardNumber('6007220000000004'), 'forbrugsforeningen')
assert($.payment.validateCardNumber('5019717010103742'), 'dankort')
assert($.payment.validateCardNumber('4111111111111111'), 'visa')
assert($.payment.validateCardNumber('4012888888881881'), 'visa')
assert($.payment.validateCardNumber('4222222222222'), 'visa')
assert($.payment.validateCardNumber('4462030000000000'), 'visa')
assert($.payment.validateCardNumber('4484070000000000'), 'visa')
assert($.payment.validateCardNumber('5555555555554444'), 'mastercard')
assert($.payment.validateCardNumber('5454545454545454'), 'mastercard')
assert($.payment.validateCardNumber('2221000002222221'), 'mastercard')
assert($.payment.validateCardNumber('378282246310005'), 'amex')
assert($.payment.validateCardNumber('371449635398431'), 'amex')
assert($.payment.validateCardNumber('378734493671000'), 'amex')
assert($.payment.validateCardNumber('30569309025904'), 'dinersclub')
assert($.payment.validateCardNumber('38520000023237'), 'dinersclub')
assert($.payment.validateCardNumber('36700102000000'), 'dinersclub')
assert($.payment.validateCardNumber('36148900647913'), 'dinersclub')
assert($.payment.validateCardNumber('6011111111111117'), 'discover')
assert($.payment.validateCardNumber('6011000990139424'), 'discover')
assert($.payment.validateCardNumber('6271136264806203568'), 'unionpay')
assert($.payment.validateCardNumber('6236265930072952775'), 'unionpay')
assert($.payment.validateCardNumber('6204679475679144515'), 'unionpay')
assert($.payment.validateCardNumber('6216657720782466507'), 'unionpay')
assert($.payment.validateCardNumber('3530111333300000'), 'jcb')
assert($.payment.validateCardNumber('3566002020360505'), 'jcb')
assert($.payment.validateCardNumber('6362970000457013'), 'elo')
assert($.payment.validateCardNumber('5066991111111118'), 'elo')
describe 'Validating a CVC', ->
it 'should fail if is empty', ->
topic = $.payment.validateCardCVC ''
assert.equal topic, false
it 'should pass if is valid', ->
topic = $.payment.validateCardCVC '123'
assert.equal topic, true
it 'should fail with non-digits', ->
topic = $.payment.validateCardNumber '12e'
assert.equal topic, false
it 'should fail with less than 3 digits', ->
topic = $.payment.validateCardNumber '12'
assert.equal topic, false
it 'should fail with more than 4 digits', ->
topic = $.payment.validateCardNumber '12345'
assert.equal topic, false
describe 'Validating an expiration date', ->
it 'should fail expires is before the current year', ->
currentTime = new Date()
topic = $.payment.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear() - 1
assert.equal topic, false
it 'that expires in the current year but before current month', ->
currentTime = new Date()
topic = $.payment.validateCardExpiry currentTime.getMonth(), currentTime.getFullYear()
assert.equal topic, false
it 'that has an invalid month', ->
currentTime = new Date()
topic = $.payment.validateCardExpiry 13, currentTime.getFullYear()
assert.equal topic, false
it 'that is this year and month', ->
currentTime = new Date()
topic = $.payment.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear()
assert.equal topic, true
it 'that is just after this month', ->
# Remember - months start with 0 in JavaScript!
currentTime = new Date()
topic = $.payment.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear()
assert.equal topic, true
it 'that is after this year', ->
currentTime = new Date()
topic = $.payment.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear() + 1
assert.equal topic, true
it 'that is a two-digit year', ->
currentTime = new Date()
topic = $.payment.validateCardExpiry currentTime.getMonth() + 1, ('' + currentTime.getFullYear())[0...2]
assert.equal topic, true
it 'that is a two-digit year in the past (i.e. 1990s)', ->
currentTime = new Date()
topic = $.payment.validateCardExpiry currentTime.getMonth() + 1, 99
assert.equal topic, false
it 'that has string numbers', ->
currentTime = new Date()
currentTime.setFullYear(currentTime.getFullYear() + 1, currentTime.getMonth() + 2)
topic = $.payment.validateCardExpiry currentTime.getMonth() + 1 + '', currentTime.getFullYear() + ''
assert.equal topic, true
it 'that has non-numbers', ->
topic = $.payment.validateCardExpiry 'h12', '3300'
assert.equal topic, false
it 'should fail if year or month is NaN', ->
topic = $.payment.validateCardExpiry '12', NaN
assert.equal topic, false
it 'should support year shorthand', ->
assert.equal $.payment.validateCardExpiry('05', '20'), true
describe 'Validating a CVC number', ->
it 'should validate a three digit number with no card type', ->
topic = $.payment.validateCardCVC('123')
assert.equal topic, true
it 'should validate a three digit number with card type amex', ->
topic = $.payment.validateCardCVC('123', 'amex')
assert.equal topic, true
it 'should validate a three digit number with card type other than amex', ->
topic = $.payment.validateCardCVC('123', 'visa')
assert.equal topic, true
it 'should not validate a four digit number with a card type other than amex', ->
topic = $.payment.validateCardCVC('1234', 'visa')
assert.equal topic, false
it 'should validate a four digit number with card type amex', ->
topic = $.payment.validateCardCVC('1234', 'amex')
assert.equal topic, true
it 'should not validate a number larger than 4 digits', ->
topic = $.payment.validateCardCVC('12344')
assert.equal topic, false
describe 'Parsing an expiry value', ->
it 'should parse string expiry', ->
topic = $.payment.cardExpiryVal('03 / 2025')
assert.deepEqual topic, month: 3, year: 2025
it 'should support shorthand year', ->
topic = $.payment.cardExpiryVal('05/04')
assert.deepEqual topic, month: 5, year: 2004
it 'should return NaN when it cannot parse', ->
topic = $.payment.cardExpiryVal('05/dd')
assert isNaN(topic.year)
describe 'Getting a card type', ->
it 'should return Visa that begins with 40', ->
topic = $.payment.cardType '4012121212121212'
assert.equal topic, 'visa'
it 'that begins with 2 should return MasterCard', ->
topic = $.payment.cardType '2221000002222221'
assert.equal topic, 'mastercard'
it 'that begins with 5 should return MasterCard', ->
topic = $.payment.cardType '5555555555554444'
assert.equal topic, 'mastercard'
it 'that begins with 34 should return American Express', ->
topic = $.payment.cardType '3412121212121212'
assert.equal topic, 'amex'
it 'that begins with 457393 should return Elo', ->
topic = $.payment.cardType '4573931212121212'
assert.equal topic, 'elo'
it 'that begins with 431274 should return Elo', ->
topic = $.payment.cardType '4312740000000000'
assert.equal topic, 'elo'
it 'that begins with 650031 should return Elo', ->
topic = $.payment.cardType '6500310000000000'
assert.equal topic, 'elo'
it 'that is not numbers should return null', ->
topic = $.payment.cardType 'aoeu'
assert.equal topic, null
it 'that has unrecognized beginning numbers should return null', ->
topic = $.payment.cardType 'aoeu'
assert.equal topic, null
it 'should return correct type for all test numbers', ->
assert.equal($.payment.cardType('4917300800000000'), 'visaelectron')
assert.equal($.payment.cardType('6759649826438453'), 'maestro')
assert.equal($.payment.cardType('6220180012340012345'), 'maestro')
assert.equal($.payment.cardType('6007220000000004'), 'forbrugsforeningen')
assert.equal($.payment.cardType('5019717010103742'), 'dankort')
assert.equal($.payment.cardType('4111111111111111'), 'visa')
assert.equal($.payment.cardType('4012888888881881'), 'visa')
assert.equal($.payment.cardType('4222222222222'), 'visa')
assert.equal($.payment.cardType('4462030000000000'), 'visa')
assert.equal($.payment.cardType('4484070000000000'), 'visa')
assert.equal($.payment.cardType('5555555555554444'), 'mastercard')
assert.equal($.payment.cardType('5454545454545454'), 'mastercard')
assert.equal($.payment.cardType('2221000002222221'), 'mastercard')
assert.equal($.payment.cardType('378282246310005'), 'amex')
assert.equal($.payment.cardType('371449635398431'), 'amex')
assert.equal($.payment.cardType('378734493671000'), 'amex')
assert.equal($.payment.cardType('30569309025904'), 'dinersclub')
assert.equal($.payment.cardType('38520000023237'), 'dinersclub')
assert.equal($.payment.cardType('36700102000000'), 'dinersclub')
assert.equal($.payment.cardType('36148900647913'), 'dinersclub')
assert.equal($.payment.cardType('6011111111111117'), 'discover')
assert.equal($.payment.cardType('6011000990139424'), 'discover')
assert.equal($.payment.cardType('6271136264806203568'), 'unionpay')
assert.equal($.payment.cardType('6236265930072952775'), 'unionpay')
assert.equal($.payment.cardType('6204679475679144515'), 'unionpay')
assert.equal($.payment.cardType('6216657720782466507'), 'unionpay')
assert.equal($.payment.cardType('3530111333300000'), 'jcb')
assert.equal($.payment.cardType('3566002020360505'), 'jcb')
assert.equal($.payment.cardType('6363689826438453'), 'elo')
describe 'Extending the card collection', ->
it 'should expose an array of standard card types', ->
cards = $.payment.cards
assert Array.isArray(cards)
visa = card for card in cards when card.type is 'visa'
assert.notEqual visa, null
it 'should support new card types', ->
wing = {
type: 'wing'
patterns: [501818]
length: [16]
luhn: false
}
$.payment.cards.unshift wing
wingCard = '5018 1818 1818 1818'
assert.equal $.payment.cardType(wingCard), 'wing'
assert.equal $.payment.validateCardNumber(wingCard), true
describe 'formatCardNumber', ->
it 'should format cc number correctly', (done) ->
$number = $('<input type=text>').payment('formatCardNumber')
$number.val('4242').prop('selectionStart', 4)
e = $.Event('keypress')
e.which = 52 # '4'
$number.trigger(e)
setTimeout ->
assert.equal $number.val(), '4242 4'
done()
it 'should format amex cc number correctly', (done) ->
$number = $('<input type=text>').payment('formatCardNumber')
$number.val('3782').prop('selectionStart', 4)
e = $.Event('keypress')
e.which = 56 # '8'
$number.trigger(e)
setTimeout ->
assert.equal $number.val(), '3782 8'
done()
it 'should format full-width cc number correctly', (done) ->
$number = $('<input type=text>').payment('formatCardNumber')
$number.val('\uff14\uff12\uff14\uff12')
e = $.Event('input')
$number.trigger(e)
setTimeout ->
assert.equal $number.val(), '4242'
done()
describe 'formatCardExpiry', ->
it 'should format month shorthand correctly', (done) ->
$expiry = $('<input type=text>').payment('formatCardExpiry')
$expiry.val('')
e = $.Event('keypress')
e.which = 52 # '4'
$expiry.trigger(e)
setTimeout ->
assert.equal $expiry.val(), '04 / '
done()
it 'should format forward slash shorthand correctly', (done) ->
$expiry = $('<input type=text>').payment('formatCardExpiry')
$expiry.val('1')
e = $.Event('keypress')
e.which = 47 # '/'
$expiry.trigger(e)
setTimeout ->
assert.equal $expiry.val(), '01 / '
done()
it 'should only allow numbers', (done) ->
$expiry = $('<input type=text>').payment('formatCardExpiry')
$expiry.val('1')
e = $.Event('keypress')
e.which = 100 # 'd'
$expiry.trigger(e)
setTimeout ->
assert.equal $expiry.val(), '1'
done()
it 'should format full-width expiry correctly', (done) ->
$expiry = $('<input type=text>').payment('formatCardExpiry')
$expiry.val('\uff10\uff18\uff11\uff15')
e = $.Event('input')
$expiry.trigger(e)
setTimeout ->
assert.equal $expiry.val(), '08 / 15'
done()
it 'should format month expiry correctly when val is past 12', (done) ->
$expiry = $('<input type=text>').payment('formatCardExpiry')
$expiry.val('1')
e = $.Event('keypress')
e.which = 52 # '4'
$expiry.trigger(e)
setTimeout ->
assert.equal $expiry.val(), '01 / 4'
done()
it 'should format month expiry corrrectly for 0 followed by single digit > 2', (done) ->
$expiry = $('<input type=text>').payment('formatCardExpiry')
$expiry.val('0')
e = $.Event('keypress')
e.which = 53 # '5'
$expiry.trigger(e)
setTimeout ->
assert.equal $expiry.val(), '05 / '
done()

View File

@ -0,0 +1,9 @@
window = require('jsdom').jsdom().defaultView
global.window = window
global.document = window.document
global.getComputedStyle = window.getComputedStyle
global.$ = require('zeptojs')
window.$ = global.$
require('../src/jquery.payment')
require('./specs')