Mwalker has uploaded a new change for review. https://gerrit.wikimedia.org/r/118055
Change subject: Introducting jQuery.Payment ...................................................................... Introducting jQuery.Payment A library by Stripe from https://github.com/stripe/jquery.payment/ for more easily creating UIs that do intelligent things with credit cards. Change-Id: I6d11aa1e9ff5183f81c93bf3d824f91552f0cbb8 --- M DonationInterface.php A modules/jquery.payment/LICENSE A modules/jquery.payment/README.md A modules/jquery.payment/jquery.payment.js 4 files changed, 731 insertions(+), 0 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/DonationInterface refs/changes/55/118055/1 diff --git a/DonationInterface.php b/DonationInterface.php index 4045eeb..ca46ef7 100644 --- a/DonationInterface.php +++ b/DonationInterface.php @@ -861,6 +861,11 @@ ) ) + $wgResourceTemplate; +$wgResourceModules['jquery.payment'] = array( + 'scripts' => 'jquery.payment/jquery.payment.js', + 'dependencies' => array( 'jquery' ) +) + $wgResourceTemplate;; + // load any rapidhtml related resources require_once( $donationinterface_dir . 'gateway_forms/rapidhtml/RapidHtmlResources.php' ); diff --git a/modules/jquery.payment/LICENSE b/modules/jquery.payment/LICENSE new file mode 100644 index 0000000..b685134 --- /dev/null +++ b/modules/jquery.payment/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2012 Stripe (a...@stripe.com) + +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. diff --git a/modules/jquery.payment/README.md b/modules/jquery.payment/README.md new file mode 100644 index 0000000..9e7d03c --- /dev/null +++ b/modules/jquery.payment/README.md @@ -0,0 +1,209 @@ +# jQuery.payment + +A general purpose library for building credit card forms, validating inputs and formatting numbers. + +For example, you can make a input act like a credit card field (with number formatting, and length restriction): + +``` javascript +$('input.cc-num').payment('formatCardNumber'); +``` + +Then, when say the payment form is submitted, you can validate the card number on the client-side like so: + +``` 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.com/jquery.payment/example). + +Supported card types are: + +* Visa +* MasterCard +* American Express +* Discover +* JCB +* Diners Club +* Maestro +* Laster +* UnionPay + +## API + +### $.fn.payment('formatCardNumber') + +Formats card numbers: + +* Including a space between every 4 digits +* Restricts input to numbers +* Limits to 16 numbers +* American Express formatting support +* Adds a class of the card type (i.e. '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` +* `discover` +* `amex` +* `dinersclub` +* `maestro` +* `laser` +* `unionpay` + +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 do any validation of the month or year, use `$.payment.validateCardExpiry(month, year)` for that. + +## Example + +Look in `./example/index.html` + +## Building + +Run `cake build` + +## Run tests + +Run `mocha --compilers coffee:coffee-script` + +## Autocomplete recommendations + +We recommend you turn autocomplete on for credit card forms, except for the CVC field. 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 [Autocomplete Types spec](http://wiki.whatwg.org/wiki/Autocomplete_Types). These are respected by a number of browsers, including Chrome. + +``` html +<input type="text" class="cc-number" pattern="\d*" autocompletetype="cc-number" placeholder="Card number" required> +``` + +Set `autocompletetype` to `cc-number` for credit card numbers, `cc-exp` for credit card expiry and `cc-csc` for the CVC (security code). + +## Mobile recommendations + +We recommend you set the `pattern` attribute which will cause the numeric keyboard to be displayed on mobiles: + +``` html +<input class="cc-number" pattern="\d*"> +``` + +You may have to turn off HTML5 validation (using the `novalidate` form attribute) when using this `pattern`, as it won't match space formatting. diff --git a/modules/jquery.payment/jquery.payment.js b/modules/jquery.payment/jquery.payment.js new file mode 100644 index 0000000..c81c6e0 --- /dev/null +++ b/modules/jquery.payment/jquery.payment.js @@ -0,0 +1,497 @@ +// Generated by CoffeeScript 1.4.0 +(function() { + var $, cardFromNumber, cardFromType, cards, defaultFormat, formatBackCardNumber, formatBackExpiry, formatCardNumber, formatExpiry, formatForwardExpiry, formatForwardSlash, hasTextSelected, luhnCheck, reFormatCardNumber, restrictCVC, restrictCardNumber, restrictExpiry, restrictNumeric, 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; }, + _this = this; + + $ = jQuery; + + $.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; + + cards = [ + { + type: 'maestro', + pattern: /^(5018|5020|5038|6304|6759|676[1-3])/, + format: defaultFormat, + length: [12, 13, 14, 15, 16, 17, 18, 19], + cvcLength: [3], + luhn: true + }, { + type: 'dinersclub', + pattern: /^(36|38|30[0-5])/, + format: defaultFormat, + length: [14], + cvcLength: [3], + luhn: true + }, { + type: 'laser', + pattern: /^(6706|6771|6709)/, + format: defaultFormat, + length: [16, 17, 18, 19], + cvcLength: [3], + luhn: true + }, { + type: 'jcb', + pattern: /^35/, + format: defaultFormat, + length: [16], + cvcLength: [3], + luhn: true + }, { + type: 'unionpay', + pattern: /^62/, + format: defaultFormat, + length: [16, 17, 18, 19], + cvcLength: [3], + luhn: false + }, { + type: 'discover', + pattern: /^(6011|65|64[4-9]|622)/, + format: defaultFormat, + length: [16], + cvcLength: [3], + luhn: true + }, { + type: 'mastercard', + pattern: /^5[1-5]/, + format: defaultFormat, + length: [16], + cvcLength: [3], + luhn: true + }, { + type: 'amex', + pattern: /^3[47]/, + format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/, + length: [15], + cvcLength: [3, 4], + luhn: true + }, { + type: 'visa', + pattern: /^4/, + format: defaultFormat, + length: [13, 14, 15, 16], + cvcLength: [3], + luhn: true + } + ]; + + cardFromNumber = function(num) { + var card, _i, _len; + num = (num + '').replace(/\D/g, ''); + for (_i = 0, _len = cards.length; _i < _len; _i++) { + card = cards[_i]; + if (card.pattern.test(num)) { + 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 ? typeof _ref.createRange === "function" ? _ref.createRange().text : void 0 : void 0 : void 0) { + return true; + } + return false; + }; + + reFormatCardNumber = function(e) { + var _this = this; + return setTimeout(function() { + var $target, value; + $target = $(e.currentTarget); + value = $target.val(); + value = $.payment.formatCardNumber(value); + return $target.val(value); + }); + }; + + 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 $target.val(value + ' ' + digit); + } else if (re.test(value + digit)) { + e.preventDefault(); + return $target.val(value + digit + ' '); + } + }; + + formatBackCardNumber = function(e) { + var $target, value; + $target = $(e.currentTarget); + value = $target.val(); + if (e.meta) { + return; + } + if (e.which !== 8) { + return; + } + if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) { + return; + } + if (/\d\s$/.test(value)) { + e.preventDefault(); + return $target.val(value.replace(/\d\s$/, '')); + } else if (/\s\d?$/.test(value)) { + e.preventDefault(); + return $target.val(value.replace(/\s\d?$/, '')); + } + }; + + 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 $target.val("0" + val + " / "); + } else if (/^\d\d$/.test(val)) { + e.preventDefault(); + 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 + " / "); + } + }; + + formatForwardSlash = function(e) { + var $target, slash, val; + slash = String.fromCharCode(e.which); + if (slash !== '/') { + return; + } + $target = $(e.currentTarget); + val = $target.val(); + if (/^\d$/.test(val) && val !== '0') { + return $target.val("0" + val + " / "); + } + }; + + formatBackExpiry = function(e) { + var $target, value; + if (e.meta) { + return; + } + $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 $target.val(value.replace(/\d(\s|\/)*$/, '')); + } else if (/\s\/\s?\d?$/.test(value)) { + e.preventDefault(); + return $target.val(value.replace(/\s\/\s?\d?$/, '')); + } + }; + + 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; + } + 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.payment('restrictNumeric'); + this.on('keypress', restrictCVC); + return this; + }; + + $.payment.fn.formatCardExpiry = function() { + this.payment('restrictNumeric'); + this.on('keypress', restrictExpiry); + this.on('keypress', formatExpiry); + this.on('keypress', formatForwardSlash); + this.on('keypress', formatForwardExpiry); + this.on('keydown', formatBackExpiry); + return this; + }; + + $.payment.fn.formatCardNumber = function() { + this.payment('restrictNumeric'); + this.on('keypress', restrictCardNumber); + this.on('keypress', formatCardNumber); + this.on('keydown', formatBackCardNumber); + this.on('keyup', setCardType); + this.on('paste', reFormatCardNumber); + return this; + }; + + $.payment.fn.restrictNumeric = function() { + this.on('keypress', restrictNumeric); + return this; + }; + + $.payment.fn.cardExpiryVal = function() { + return $.payment.cardExpiryVal($(this).val()); + }; + + $.payment.cardExpiryVal = function(value) { + var month, prefix, year, _ref; + value = value.replace(/\s/g, ''); + _ref = value.split('/', 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, prefix, _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 (!(parseInt(month, 10) <= 12)) { + return false; + } + if (year.length === 2) { + prefix = (new Date).getFullYear(); + prefix = prefix.toString().slice(0, 2); + year = prefix + year; + } + 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 _ref, _ref1; + cvc = $.trim(cvc); + if (!/^\d+$/.test(cvc)) { + return false; + } + if (type) { + return _ref = cvc.length, __indexOf.call((_ref1 = cardFromType(type)) != null ? _ref1.cvcLength : void 0, _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; + card = cardFromNumber(num); + if (!card) { + return num; + } + upperLength = card.length[card.length.length - 1]; + num = num.replace(/\D/g, ''); + num = num.slice(0, +upperLength + 1 || 9e9); + if (card.format.global) { + return (_ref = num.match(card.format)) != null ? _ref.join(' ') : void 0; + } else { + groups = card.format.exec(num); + if (groups != null) { + groups.shift(); + } + return groups != null ? groups.join(' ') : void 0; + } + }; + +}).call(this); -- To view, visit https://gerrit.wikimedia.org/r/118055 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I6d11aa1e9ff5183f81c93bf3d824f91552f0cbb8 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/DonationInterface Gerrit-Branch: master Gerrit-Owner: Mwalker <mwal...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits