882 lines
21 KiB
JavaScript
882 lines
21 KiB
JavaScript
|
angular.module('angularPayments', []);;angular.module('angularPayments')
|
||
|
|
||
|
.factory('Common', [function(){
|
||
|
|
||
|
var ret = {};
|
||
|
|
||
|
// expiry is a string "mm / yy[yy]"
|
||
|
ret.parseExpiry = function(value){
|
||
|
var month, prefix, year, _ref;
|
||
|
|
||
|
value = value || '';
|
||
|
|
||
|
value = value.replace(/\s/g, '');
|
||
|
_ref = value.split('/', 2);
|
||
|
month = _ref[0];
|
||
|
year = _ref[1];
|
||
|
|
||
|
if (year && year.length === 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
|
||
|
};
|
||
|
};
|
||
|
|
||
|
return ret;
|
||
|
|
||
|
}]);
|
||
|
;angular.module('angularPayments')
|
||
|
|
||
|
.factory('Cards', [function(){
|
||
|
|
||
|
var defaultFormat = /(\d{1,4})/g;
|
||
|
var defaultInputFormat = /(?:^|\s)(\d{4})$/;
|
||
|
|
||
|
var cards = [
|
||
|
{
|
||
|
type: 'maestro',
|
||
|
pattern: /^(5018|5020|5038|6304|6759|676[1-3])/,
|
||
|
format: defaultFormat,
|
||
|
inputFormat: defaultInputFormat,
|
||
|
length: [12, 13, 14, 15, 16, 17, 18, 19],
|
||
|
cvcLength: [3],
|
||
|
luhn: true
|
||
|
}, {
|
||
|
type: 'dinersclub',
|
||
|
pattern: /^(36|38|30[0-5])/,
|
||
|
format: defaultFormat,
|
||
|
inputFormat: defaultInputFormat,
|
||
|
length: [14],
|
||
|
cvcLength: [3],
|
||
|
luhn: true
|
||
|
}, {
|
||
|
type: 'laser',
|
||
|
pattern: /^(6706|6771|6709)/,
|
||
|
format: defaultFormat,
|
||
|
inputFormat: defaultInputFormat,
|
||
|
length: [16, 17, 18, 19],
|
||
|
cvcLength: [3],
|
||
|
luhn: true
|
||
|
}, {
|
||
|
type: 'jcb',
|
||
|
pattern: /^35/,
|
||
|
format: defaultFormat,
|
||
|
inputFormat: defaultInputFormat,
|
||
|
length: [16],
|
||
|
cvcLength: [3],
|
||
|
luhn: true
|
||
|
}, {
|
||
|
type: 'unionpay',
|
||
|
pattern: /^62/,
|
||
|
format: defaultFormat,
|
||
|
inputFormat: defaultInputFormat,
|
||
|
length: [16, 17, 18, 19],
|
||
|
cvcLength: [3],
|
||
|
luhn: false
|
||
|
}, {
|
||
|
type: 'discover',
|
||
|
pattern: /^(6011|65|64[4-9]|622)/,
|
||
|
format: defaultFormat,
|
||
|
inputFormat: defaultInputFormat,
|
||
|
length: [16],
|
||
|
cvcLength: [3],
|
||
|
luhn: true
|
||
|
}, {
|
||
|
type: 'mastercard',
|
||
|
pattern: /^5[1-5]/,
|
||
|
format: defaultFormat,
|
||
|
inputFormat: defaultInputFormat,
|
||
|
length: [16],
|
||
|
cvcLength: [3],
|
||
|
luhn: true
|
||
|
}, {
|
||
|
type: 'amex',
|
||
|
pattern: /^3[47]/,
|
||
|
format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/,
|
||
|
inputFormat: /^(\d{4}|\d{4}\s\d{6})$/,
|
||
|
length: [15],
|
||
|
cvcLength: [3, 4],
|
||
|
luhn: true
|
||
|
}, {
|
||
|
type: 'visa',
|
||
|
pattern: /^4/,
|
||
|
format: defaultFormat,
|
||
|
inputFormat: defaultInputFormat,
|
||
|
length: [13, 14, 15, 16],
|
||
|
cvcLength: [3],
|
||
|
luhn: true
|
||
|
}
|
||
|
];
|
||
|
|
||
|
|
||
|
var _fromNumber = 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;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
};
|
||
|
|
||
|
var _fromType = function(type) {
|
||
|
var card, i, len;
|
||
|
|
||
|
for (i = 0, len = cards.length; i < len; i++) {
|
||
|
|
||
|
card = cards[i];
|
||
|
|
||
|
if (card.type === type) {
|
||
|
return card;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return {
|
||
|
fromNumber: function(val) { return _fromNumber(val); },
|
||
|
fromType: function(val) { return _fromType(val); },
|
||
|
defaultFormat: function() { return defaultFormat; },
|
||
|
defaultInputFormat: function() { return defaultInputFormat; }
|
||
|
};
|
||
|
|
||
|
}]);
|
||
|
;/**
|
||
|
* Format
|
||
|
*/
|
||
|
angular.module('angularPayments')
|
||
|
|
||
|
.factory('_Format', ['Cards', 'Common', '$filter', function(Cards, Common, $filter){
|
||
|
|
||
|
var _formats = {};
|
||
|
|
||
|
var _hasTextSelected = function($target) {
|
||
|
var ref;
|
||
|
|
||
|
if (($target.prop('selectionStart') !== null) && $target.prop('selectionStart') !== $target.prop('selectionEnd')) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
if (document.selection) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
};
|
||
|
|
||
|
// card formatting
|
||
|
|
||
|
var isInvalidKey = function(e) {
|
||
|
var digit = String.fromCharCode(e.which);
|
||
|
return !/^\d+$/.test(digit) && !e.metaKey && e.charCode !== 0 && !e.ctrlKey;
|
||
|
};
|
||
|
|
||
|
var _formatCardNumber = function(e) {
|
||
|
var $target, card, digit, length, re, upperLength, value;
|
||
|
|
||
|
digit = String.fromCharCode(e.which);
|
||
|
$target = angular.element(e.currentTarget);
|
||
|
value = $target.val();
|
||
|
card = Cards.fromNumber(value + digit);
|
||
|
length = (value.replace(/\D/g, '') + digit).length;
|
||
|
|
||
|
upperLength = 16;
|
||
|
|
||
|
// Catch delete, tab, backspace, arrows, etc..
|
||
|
if (e.which === 8 || e.which === 0) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (card) {
|
||
|
upperLength = card.length[card.length.length - 1];
|
||
|
}
|
||
|
|
||
|
|
||
|
if (isInvalidKey(e)) {
|
||
|
e.preventDefault();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (($target.prop('selectionStart') !== null) && $target.prop('selectionStart') !== value.length) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
re = Cards.defaultInputFormat();
|
||
|
if (card) {
|
||
|
re = card.inputFormat;
|
||
|
}
|
||
|
|
||
|
if (length >= upperLength) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (re.test(value)) {
|
||
|
e.preventDefault();
|
||
|
return $target.val(value + ' ' + digit);
|
||
|
|
||
|
} else if (re.test(value + digit)) {
|
||
|
e.preventDefault();
|
||
|
return $target.val(value + digit + ' ');
|
||
|
}
|
||
|
};
|
||
|
|
||
|
var _restrictCardNumber = function(e) {
|
||
|
var $target, card, digit, value;
|
||
|
|
||
|
$target = angular.element(e.currentTarget);
|
||
|
digit = String.fromCharCode(e.which);
|
||
|
|
||
|
// Catch delete, tab, backspace, arrows, etc..
|
||
|
if (e.which === 8 || e.which === 0) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if(!/^\d+$/.test(digit)) {
|
||
|
e.preventDefault();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if(_hasTextSelected($target)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
value = ($target.val() + digit).replace(/\D/g, '');
|
||
|
card = Cards.fromNumber(value);
|
||
|
|
||
|
if(card) {
|
||
|
if(value.length > card.length[card.length.length - 1]){
|
||
|
e.preventDefault();
|
||
|
}
|
||
|
} else {
|
||
|
if(value.length > 16){
|
||
|
e.preventDefault();
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
var _formatBackCardNumber = function(e) {
|
||
|
var $target, value;
|
||
|
|
||
|
$target = angular.element(e.currentTarget);
|
||
|
value = $target.val();
|
||
|
|
||
|
if(e.metaKey) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if(e.which !== 8) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if(($target.prop('selectionStart') !== null) && $target.prop('selectionStart') !== value.length) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if(/\d\s$/.test(value) && !e.metaKey && e.keyCode >= 46) {
|
||
|
e.preventDefault();
|
||
|
return $target.val(value.replace(/\d\s$/, ''));
|
||
|
} else if (/\s\d?$/.test(value)) {
|
||
|
e.preventDefault();
|
||
|
return $target.val(value.replace(/\s\d?$/, ''));
|
||
|
}
|
||
|
};
|
||
|
|
||
|
var _getFormattedCardNumber = function(num) {
|
||
|
var card, groups, upperLength, ref;
|
||
|
|
||
|
card = Cards.fromNumber(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;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
var _reFormatCardNumber = function(e) {
|
||
|
return setTimeout(function() {
|
||
|
var $target, value;
|
||
|
$target = angular.element(e.target);
|
||
|
|
||
|
value = $target.val();
|
||
|
value = _getFormattedCardNumber(value);
|
||
|
return $target.val(value);
|
||
|
});
|
||
|
};
|
||
|
|
||
|
var _parseCardNumber = function(value) {
|
||
|
return value !== null && value !== undefined ? value.replace(/\s/g, '') : value;
|
||
|
};
|
||
|
|
||
|
_formats.card = function(elem, ctrl){
|
||
|
elem.bind('keypress', _restrictCardNumber);
|
||
|
elem.bind('keypress', _formatCardNumber);
|
||
|
elem.bind('keydown', _formatBackCardNumber);
|
||
|
elem.bind('paste', _reFormatCardNumber);
|
||
|
|
||
|
ctrl.$parsers.push(_parseCardNumber);
|
||
|
ctrl.$formatters.push(_getFormattedCardNumber);
|
||
|
};
|
||
|
|
||
|
|
||
|
// cvc
|
||
|
|
||
|
var _formatCVC = function(e){
|
||
|
var $target, digit, value;
|
||
|
|
||
|
$target = angular.element(e.currentTarget);
|
||
|
digit = String.fromCharCode(e.which);
|
||
|
|
||
|
// Catch delete, tab, backspace, arrows, etc..
|
||
|
if (e.which === 8 || e.which === 0) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (isInvalidKey(e)) {
|
||
|
e.preventDefault();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if(_hasTextSelected($target)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
value = $target.val() + digit;
|
||
|
|
||
|
if(value.length <= 4){
|
||
|
return;
|
||
|
} else {
|
||
|
e.preventDefault();
|
||
|
return;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
_formats.cvc = function(elem){
|
||
|
elem.bind('keypress', _formatCVC);
|
||
|
};
|
||
|
|
||
|
|
||
|
// expiry
|
||
|
|
||
|
var _restrictExpiry = function(e) {
|
||
|
var $target, digit, value;
|
||
|
|
||
|
$target = angular.element(e.currentTarget);
|
||
|
digit = String.fromCharCode(e.which);
|
||
|
|
||
|
if (isInvalidKey(e)) {
|
||
|
e.preventDefault();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if(_hasTextSelected($target)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
value = $target.val() + digit;
|
||
|
value = value.replace(/\D/g, '');
|
||
|
|
||
|
if (value.length > 6) {
|
||
|
e.preventDefault();
|
||
|
return;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
var _formatExpiry = function(e) {
|
||
|
var $target, digit, val;
|
||
|
|
||
|
digit = String.fromCharCode(e.which);
|
||
|
|
||
|
if (isInvalidKey(e)) {
|
||
|
e.preventDefault();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$target = angular.element(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 + " / ");
|
||
|
|
||
|
}
|
||
|
};
|
||
|
|
||
|
var _formatForwardExpiry = function(e) {
|
||
|
var $target, digit, val;
|
||
|
|
||
|
digit = String.fromCharCode(e.which);
|
||
|
|
||
|
if (isInvalidKey(e)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$target = angular.element(e.currentTarget);
|
||
|
val = $target.val();
|
||
|
|
||
|
if (/^\d\d$/.test(val)) {
|
||
|
return $target.val("" + val + " / ");
|
||
|
}
|
||
|
};
|
||
|
|
||
|
var _formatForwardSlash = function(e) {
|
||
|
var $target, slash, val;
|
||
|
|
||
|
slash = String.fromCharCode(e.which);
|
||
|
|
||
|
if (slash !== '/') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$target = angular.element(e.currentTarget);
|
||
|
val = $target.val();
|
||
|
|
||
|
if (/^\d$/.test(val) && val !== '0') {
|
||
|
return $target.val("0" + val + " / ");
|
||
|
}
|
||
|
};
|
||
|
|
||
|
var _formatBackExpiry = function(e) {
|
||
|
var $target, value;
|
||
|
|
||
|
if (e.meta || e.metaKey) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$target = angular.element(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?$/, ''));
|
||
|
|
||
|
}
|
||
|
};
|
||
|
|
||
|
var _parseExpiry = function(value) {
|
||
|
if(value !== null) {
|
||
|
var obj = Common.parseExpiry(value);
|
||
|
var expiry = new Date(obj.year, obj.month-1);
|
||
|
return $filter('date')(expiry, 'MM/yyyy');
|
||
|
}
|
||
|
return null;
|
||
|
};
|
||
|
|
||
|
var _getFormattedExpiry = function(value) {
|
||
|
if(value !== null) {
|
||
|
var obj = Common.parseExpiry(value);
|
||
|
var expiry = new Date(obj.year, obj.month-1);
|
||
|
return $filter('date')(expiry, 'MM / yyyy');
|
||
|
}
|
||
|
return null;
|
||
|
};
|
||
|
|
||
|
|
||
|
_formats.expiry = function(elem, ctrl){
|
||
|
elem.bind('keypress', _restrictExpiry);
|
||
|
elem.bind('keypress', _formatExpiry);
|
||
|
elem.bind('keypress', _formatForwardSlash);
|
||
|
elem.bind('keypress', _formatForwardExpiry);
|
||
|
elem.bind('keydown', _formatBackExpiry);
|
||
|
|
||
|
ctrl.$parsers.push(_parseExpiry);
|
||
|
ctrl.$formatters.push(_getFormattedExpiry);
|
||
|
};
|
||
|
|
||
|
return function(type, elem, ctrl){
|
||
|
var types, errstr;
|
||
|
|
||
|
if(!_formats[type]){
|
||
|
|
||
|
types = Object.keys(_formats);
|
||
|
|
||
|
errstr = 'Unknown type for formatting: "'+type+'". ';
|
||
|
errstr += 'Should be one of: "'+types.join('", "')+'"';
|
||
|
|
||
|
throw errstr;
|
||
|
}
|
||
|
return _formats[type](elem, ctrl);
|
||
|
};
|
||
|
|
||
|
}])
|
||
|
|
||
|
.directive('paymentsFormat', ['$window', '_Format', function($window, _Format){
|
||
|
return {
|
||
|
restrict: 'A',
|
||
|
require: 'ngModel',
|
||
|
link: function(scope, elem, attr, ctrl){
|
||
|
_Format(attr.paymentsFormat, elem, ctrl);
|
||
|
}
|
||
|
};
|
||
|
}]);
|
||
|
;angular.module('angularPayments')
|
||
|
|
||
|
|
||
|
|
||
|
.factory('_Validate', ['Cards', 'Common', '$parse', function(Cards, Common, $parse){
|
||
|
|
||
|
var __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; };
|
||
|
|
||
|
var _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;
|
||
|
};
|
||
|
|
||
|
var _validators = {};
|
||
|
|
||
|
_validators.cvc = function(cvc, ctrl, scope, attr){
|
||
|
var ref, ref1;
|
||
|
|
||
|
// valid if empty - let ng-required handle empty
|
||
|
if(!cvc) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
if (!/^\d+$/.test(cvc)) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
var type;
|
||
|
if(attr.paymentsTypeModel) {
|
||
|
var typeModel = $parse(attr.paymentsTypeModel);
|
||
|
type = typeModel(scope);
|
||
|
}
|
||
|
|
||
|
if (type) {
|
||
|
return ref = cvc.length, __indexOf.call((ref1 = Cards.fromType(type)) !== null ? ref1.cvcLength : void 0, ref) >= 0;
|
||
|
} else {
|
||
|
return cvc.length >= 3 && cvc.length <= 4;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
_validators.card = function(num, ctrl, scope, attr){
|
||
|
var card, ref, typeModel, ret;
|
||
|
|
||
|
if(attr.paymentsTypeModel) {
|
||
|
typeModel = $parse(attr.paymentsTypeModel);
|
||
|
}
|
||
|
|
||
|
var clearCard = function(){
|
||
|
if(typeModel) {
|
||
|
typeModel.assign(scope, null);
|
||
|
}
|
||
|
ctrl.$card = null;
|
||
|
};
|
||
|
|
||
|
// valid if empty - let ng-required handle empty
|
||
|
if(!num){
|
||
|
clearCard();
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
num = (num + '').replace(/\s+|-/g, '');
|
||
|
|
||
|
if (!/^\d+$/.test(num)) {
|
||
|
clearCard();
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
card = Cards.fromNumber(num);
|
||
|
|
||
|
if(!card) {
|
||
|
clearCard();
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
ctrl.$card = angular.copy(card);
|
||
|
|
||
|
if(typeModel) {
|
||
|
typeModel.assign(scope, card.type);
|
||
|
}
|
||
|
|
||
|
var length = 16;
|
||
|
switch (card.type) {
|
||
|
case 'amex':
|
||
|
length = 15;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
ret = (ref = num.length, __indexOf.call(card.length, ref) >= 0) && num.length === length && (card.luhn === false || _luhnCheck(num));
|
||
|
|
||
|
return ret;
|
||
|
};
|
||
|
|
||
|
_validators.expiry = function(val){
|
||
|
var month, year, obj;
|
||
|
// valid if empty - let ng-required handle empty
|
||
|
if(!val) return true;
|
||
|
|
||
|
obj = Common.parseExpiry(val);
|
||
|
|
||
|
month = obj.month;
|
||
|
year = obj.year;
|
||
|
|
||
|
var currentTime, expiry, prefix;
|
||
|
|
||
|
if (!(month && year)) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
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;
|
||
|
};
|
||
|
|
||
|
return function(type, val, ctrl, scope, attr){
|
||
|
var types, errstr;
|
||
|
if(!_validators[type]){
|
||
|
|
||
|
types = Object.keys(_validators);
|
||
|
|
||
|
errstr = 'Unknown type for validation: "'+type+'". ';
|
||
|
errstr += 'Should be one of: "'+types.join('", "')+'"';
|
||
|
|
||
|
throw errstr;
|
||
|
}
|
||
|
return _validators[type](val, ctrl, scope, attr);
|
||
|
};
|
||
|
}])
|
||
|
|
||
|
|
||
|
.factory('_ValidateWatch', ['_Validate', function(_Validate){
|
||
|
|
||
|
var _validatorWatches = {};
|
||
|
|
||
|
_validatorWatches.cvc = function(type, ctrl, scope, attr){
|
||
|
if(attr.paymentsTypeModel) {
|
||
|
scope.$watch(attr.paymentsTypeModel, function(newVal, oldVal) {
|
||
|
if(newVal !== oldVal) {
|
||
|
var valid = _Validate(type, ctrl.$modelValue, ctrl, scope, attr);
|
||
|
ctrl.$setValidity(type, valid);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return function(type, ctrl, scope, attr){
|
||
|
if(_validatorWatches[type]){
|
||
|
return _validatorWatches[type](type, ctrl, scope, attr);
|
||
|
}
|
||
|
};
|
||
|
}])
|
||
|
|
||
|
.directive('paymentsValidate', ['$window', '_Validate', '_ValidateWatch', function($window, _Validate, _ValidateWatch){
|
||
|
return {
|
||
|
restrict: 'A',
|
||
|
require: 'ngModel',
|
||
|
link: function(scope, elem, attr, ctrl){
|
||
|
|
||
|
var type = attr.paymentsValidate;
|
||
|
|
||
|
_ValidateWatch(type, ctrl, scope, attr);
|
||
|
|
||
|
var validateFn = function(val) {
|
||
|
var valid = _Validate(type, val, ctrl, scope, attr);
|
||
|
ctrl.$setValidity(type, valid);
|
||
|
return valid ? val : undefined;
|
||
|
};
|
||
|
|
||
|
ctrl.$formatters.push(validateFn);
|
||
|
ctrl.$parsers.push(validateFn);
|
||
|
|
||
|
}
|
||
|
};
|
||
|
}])
|
||
|
|
||
|
.directive('paymentsLength', [function() {
|
||
|
return {
|
||
|
require: 'ngModel',
|
||
|
link: function(scope, elem, attr, modelCtrl) {
|
||
|
modelCtrl.$parsers.push(function validateLength(value) {
|
||
|
if (attr.paymentsLength === 'card') {
|
||
|
var rawNumber = '';
|
||
|
var minlength = scope.type === 'amex' ? 15 : 16;
|
||
|
if (modelCtrl.$viewValue) {
|
||
|
rawNumber = modelCtrl.$viewValue.replace(/\s/g, '');
|
||
|
}
|
||
|
modelCtrl.$setValidity('length', rawNumber.length >= minlength);
|
||
|
}
|
||
|
return value;
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
}]);
|
||
|
;/**
|
||
|
* Stripe Form
|
||
|
*/
|
||
|
angular.module('angularPayments')
|
||
|
|
||
|
.directive('stripeForm', ['$window', '$parse', 'Common', function($window, $parse, Common) {
|
||
|
|
||
|
// directive intercepts form-submission, obtains Stripe's cardToken using stripe.js
|
||
|
// and then passes that to callback provided in stripeForm, attribute.
|
||
|
|
||
|
// data that is sent to stripe is filtered from scope, looking for valid values to
|
||
|
// send and converting camelCase to snake_case, e.g expMonth -> exp_month
|
||
|
|
||
|
|
||
|
// filter valid stripe-values from scope and convert them from camelCase to snake_case
|
||
|
var _getDataToSend = function(data){
|
||
|
|
||
|
var possibleKeys = ['number', 'expMonth', 'expYear',
|
||
|
'cvc', 'name','addressLine1',
|
||
|
'addressLine2', 'addressCity',
|
||
|
'addressState', 'addressZip',
|
||
|
'addressCountry'];
|
||
|
|
||
|
var camelToSnake = function(str){
|
||
|
return str.replace(/([A-Z])/g, function(m){
|
||
|
return "_"+m.toLowerCase();
|
||
|
});
|
||
|
};
|
||
|
|
||
|
var ret = {};
|
||
|
|
||
|
for(var i in possibleKeys){
|
||
|
if(data.hasOwnProperty(possibleKeys[i])){
|
||
|
ret[camelToSnake(possibleKeys[i])] = angular.copy(data[possibleKeys[i]]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
ret.number = (ret.number || '').replace(/ /g,'');
|
||
|
|
||
|
return ret;
|
||
|
};
|
||
|
|
||
|
return {
|
||
|
restrict: 'A',
|
||
|
link: function(scope, elem, attr) {
|
||
|
|
||
|
if(!$window.Stripe){
|
||
|
throw 'stripeForm requires that you have stripe.js installed. Include https://js.stripe.com/v2/ into your html.';
|
||
|
}
|
||
|
|
||
|
var form = angular.element(elem);
|
||
|
|
||
|
form.bind('submit', function() {
|
||
|
|
||
|
var expMonthUsed = scope.expMonth ? true : false;
|
||
|
var expYearUsed = scope.expYear ? true : false;
|
||
|
|
||
|
if(!(expMonthUsed && expYearUsed)){
|
||
|
var exp = Common.parseExpiry(scope.expiry);
|
||
|
scope.expMonth = exp.month;
|
||
|
scope.expYear = exp.year;
|
||
|
}
|
||
|
|
||
|
var button = form.find('button');
|
||
|
button.prop('disabled', true);
|
||
|
|
||
|
if(form.hasClass('ng-valid')) {
|
||
|
|
||
|
$window.Stripe.createToken(_getDataToSend(scope), function() {
|
||
|
var args = arguments;
|
||
|
scope.$apply(function() {
|
||
|
scope[attr.stripeForm].apply(scope, args);
|
||
|
});
|
||
|
button.prop('disabled', false);
|
||
|
|
||
|
});
|
||
|
|
||
|
} else {
|
||
|
scope.$apply(function() {
|
||
|
scope[attr.stripeForm].apply(scope, [400, {error: 'Invalid form submitted.'}]);
|
||
|
});
|
||
|
button.prop('disabled', false);
|
||
|
}
|
||
|
|
||
|
scope.expMonth = null;
|
||
|
scope.expYear = null;
|
||
|
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
}]);
|