├── CHANGELOG.md ├── .eslintignore ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .npmignore ├── .gitignore ├── config ├── .eslintrc ├── banner.js ├── build.js ├── entry.js └── bundle.js ├── .eslintrc ├── webpack.config.js ├── dist ├── README.md ├── vue-credit-card-validation.min.js ├── vue-credit-card-validation.esm.js ├── vue-credit-card-validation.common.js └── vue-credit-card-validation.js ├── LICENSE ├── src ├── index.js ├── cards.js ├── format.js ├── validation.js └── utils.js ├── example ├── demo.css └── index.html ├── package.json └── README.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | config/*.js 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.log 3 | *.swp 4 | *.yml 5 | coverage 6 | docs/_book 7 | config 8 | dist/*.map 9 | lib 10 | test 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | coverage 3 | dist/*.gz 4 | docs/_book 5 | test/e2e/report 6 | test/e2e/screenshots 7 | node_modules 8 | .DS_Store 9 | *.log 10 | *.swp 11 | *~ 12 | -------------------------------------------------------------------------------- /config/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "process": true 4 | }, 5 | "extends": "vue", 6 | "rules": { 7 | "no-multiple-empty-lines": [2, {"max": 2}], 8 | "no-console": 0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "plugin:vue-libs/recommended" 5 | ], 6 | "rules": { 7 | "object-curly-spacing": ["error", "always"], 8 | "no-multiple-empty-lines": ["error", { "max": 2, "maxBOF": 1 }],transforms: { forOf: false } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /config/banner.js: -------------------------------------------------------------------------------- 1 | const pack = require('../package.json') 2 | const version = process.env.VERSION || pack.version 3 | 4 | module.exports = 5 | '/*!\n' + 6 | ` * ${pack.name} v${version} \n` + 7 | ` * (c) ${new Date().getFullYear()} ${pack.author.name}\n` + 8 | ` * Released under the ${pack.license} License.\n` + 9 | ' */' 10 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // webpack.config.js 2 | const { VueLoaderPlugin } = require('vue-loader') 3 | 4 | module.exports = { 5 | module: { 6 | rules: [ 7 | // ... other rules 8 | { 9 | test: /\.vue$/, 10 | loader: 'vue-loader' 11 | } 12 | ] 13 | }, 14 | plugins: [ 15 | // make sure to include the plugin! 16 | new VueLoaderPlugin() 17 | ] 18 | } -------------------------------------------------------------------------------- /config/build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const exist = fs.existsSync 3 | const mkdir = fs.mkdirSync 4 | const getAllEntries = require('./entry').getAllEntries 5 | const build = require('./bundle') 6 | 7 | if (!exist('dist')) { 8 | mkdir('dist') 9 | } 10 | 11 | let entries = getAllEntries() 12 | 13 | // filter entries via command line arg 14 | if (process.argv[2]) { 15 | const filters = process.argv[2].split(',') 16 | entries = entries.filter(b => { 17 | return filters.some(f => b.dest.indexOf(f) > -1) 18 | }) 19 | } 20 | 21 | build(entries) 22 | -------------------------------------------------------------------------------- /dist/README.md: -------------------------------------------------------------------------------- 1 | ## Explanation of Build Files 2 | 3 | - UMD: vue-stripe-payment.js 4 | - CommonJS: vue-stripe-payment.common.js 5 | - ES Module: vue-stripe-payment.esm.js 6 | 7 | ### Terms 8 | 9 | - **[UMD](https://github.com/umdjs/umd)**: UMD builds can be used directly in the browser via a ` 12 | 13 | 14 | 15 | 16 | 17 |
18 | 22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | 30 |
31 |
32 | 33 |
34 | 35 |
36 |
37 | {{ cardErrors.cardNumber }} 38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | 46 | 47 |
48 | {{ cardErrors.cardExpiry }} 49 |
50 |
51 |
52 |
53 |
54 | 55 | 56 |
57 | {{ cardErrors.cardCvc }} 58 |
59 |
60 |
61 |
62 |
63 | 64 | 65 |
66 |
67 |
68 |
69 |
70 | 71 | 72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
Computed Values
80 |
81 |
82 |
    83 |
  • Card Brand: (awating input...){{ cardBrand }}
  • 84 |
  • Brand Classname: {{ cardBrandClass }}
  • 85 |
  • Number: {{ cardNumber }}
  • 86 |
  • Expiration: {{ cardExpiry }}
  • 87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
Validation Errors:
95 |
96 |
97 |
    98 |
  • {{ err }}
  • 99 |
  • No errors.
  • 100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
Test Data:
108 |
109 |
110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 |
Card NumberBrand
4242 4242 4242 4242Visa
5555 5555 5555 4444Mastercard
3782 822463 10005Am Ex
6011 1111 1111 1117Discover
3056 9309 0259 04Diners Club
3566 0020 2036 0505JCB
6200 0000 0000 0005UnionPay
148 |
149 |
150 |
151 |
152 |
153 |
154 |

155 | View on Github 156 |

157 |
158 |
159 |
160 |
161 |
162 |
163 | 164 | 261 | 262 | 263 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import { default as cards } from './cards.js'; 2 | import { default as validation } from './validation.js'; 3 | 4 | const cardFormatUtils = { 5 | 6 | cardFromNumber : function (num) { 7 | num = (num + '').replace(/\D/g, ''); 8 | for (let i in cards) { 9 | for (let j in cards[i].patterns) { 10 | let p = cards[i].patterns[j] + ''; 11 | if (num.substr(0, p.length) === p) { return cards[i]; } 12 | } 13 | } 14 | }, 15 | 16 | cardFromType: function (type) { 17 | for (let i in cards) { if (cards[i].type === type) { return cards[i]; } } 18 | }, 19 | 20 | luhnCheck: function (num) { 21 | let odd = true; 22 | let sum = 0; 23 | 24 | let digits = (num + '').split('').reverse(); 25 | 26 | for (let i in digits) { 27 | let digit = parseInt(digits[i], 10); 28 | if (odd = !odd) { digit *= 2; } 29 | if (digit > 9) { digit -= 9; } 30 | sum += digit; 31 | } 32 | 33 | return (sum % 10) === 0; 34 | }, 35 | 36 | hasTextSelected: function (target) { 37 | // If some text is selected 38 | if ((target.selectionStart != null) && 39 | (target.selectionStart !== target.selectionEnd)) { return true; } 40 | 41 | // If some text is selected in IE 42 | if (cardFormatUtils.__guard__(typeof document !== 'undefined' && document !== null ? document.selection : undefined, x => x.createRange) != null) { 43 | if (document.selection.createRange().text) { return true; } 44 | } 45 | 46 | return false; 47 | }, 48 | 49 | // Private 50 | 51 | // Safe Val 52 | 53 | safeVal: function (value, target, e) { 54 | if (e.inputType === 'deleteContentBackward') { 55 | return; 56 | } 57 | let cursor; 58 | try { 59 | cursor = target.selectionStart; 60 | } catch (error) { 61 | cursor = null; 62 | } 63 | let last = target.value; 64 | target.value = value; 65 | value = target.value; 66 | if ((cursor !== null) && document.activeElement == target) { 67 | if (cursor === last.length) { cursor = target.value.length; } 68 | 69 | // This hack looks for scenarios where we are changing an input's value such 70 | // that "X| " is replaced with " |X" (where "|" is the cursor). In those 71 | // scenarios, we want " X|". 72 | // 73 | // For example: 74 | // 1. Input field has value "4444| " 75 | // 2. User types "1" 76 | // 3. Input field has value "44441| " 77 | // 4. Reformatter changes it to "4444 |1" 78 | // 5. By incrementing the cursor, we make it "4444 1|" 79 | // 80 | // This is awful, and ideally doesn't go here, but given the current design 81 | // of the system there does not appear to be a better solution. 82 | // 83 | // Note that we can't just detect when the cursor-1 is " ", because that 84 | // would incorrectly increment the cursor when backspacing, e.g. pressing 85 | // backspace in this scenario: "4444 1|234 5". 86 | if (last !== value) { 87 | let prevPair = last.slice(cursor - 1, +cursor + 1 || undefined); 88 | let currPair = target.value.slice(cursor - 1, +cursor + 1 || undefined); 89 | let digit = value[cursor]; 90 | if (/\d/.test(digit) && 91 | (prevPair === `${digit} `) && (currPair === ` ${digit}`)) { cursor = cursor + 1; } 92 | } 93 | 94 | target.selectionStart = cursor; 95 | return target.selectionEnd = cursor; 96 | } 97 | }, 98 | 99 | // Replace Full-Width Chars 100 | 101 | replaceFullWidthChars: function (str) { 102 | if (str == null) { str = ''; } 103 | let fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19'; 104 | let halfWidth = '0123456789'; 105 | 106 | let value = ''; 107 | let chars = str.split(''); 108 | 109 | // Avoid using reserved word `char` 110 | for (let i in chars) { 111 | let idx = fullWidth.indexOf(chars[i]); 112 | if (idx > -1) { 113 | chars[i] = halfWidth[idx]; 114 | } 115 | value += chars[i]; 116 | } 117 | 118 | return value; 119 | }, 120 | 121 | // Format Numeric 122 | 123 | reFormatNumeric: function (e) { 124 | let target = e.currentTarget; 125 | return setTimeout(function () { 126 | let value = target.value; 127 | value = cardFormatUtils.replaceFullWidthChars(value); 128 | value = value.replace(/\D/g, ''); 129 | return cardFormatUtils.safeVal(value, target, e); 130 | }); 131 | }, 132 | 133 | // Format Card Number 134 | 135 | reFormatCardNumber: function (e) { 136 | let target = e.currentTarget; 137 | return setTimeout(() => { 138 | let value = target.value; 139 | value = cardFormatUtils.replaceFullWidthChars(value); 140 | value = validation.formatCardNumber(value); 141 | return cardFormatUtils.safeVal(value, target, e); 142 | }); 143 | }, 144 | 145 | formatCardNumber: function (e) { 146 | // Only format if input is a number 147 | let re; 148 | let digit = String.fromCharCode(e.which); 149 | if (!/^\d+$/.test(digit)) { return; } 150 | 151 | let target = e.currentTarget; 152 | let value = target.value; 153 | let card = cardFormatUtils.cardFromNumber(value + digit); 154 | let length = (value.replace(/\D/g, '') + digit); 155 | 156 | let upperLength = 16; 157 | if (card) { upperLength = card.length[card.length.length - 1]; } 158 | if (length >= upperLength) { return; } 159 | 160 | // Return if focus isn't at the end of the text 161 | if ((target.selectionStart != null) && 162 | (target.selectionStart !== value.length)) { return; } 163 | 164 | if (card && (card.type === 'amex')) { 165 | // AMEX cards are formatted differently 166 | re = /^(\d{4}|\d{4}\s\d{6})$/; 167 | } else { 168 | re = /(?:^|\s)(\d{4})$/; 169 | } 170 | 171 | // If '4242' + 4 172 | if (re.test(value + digit)) { 173 | e.preventDefault(); 174 | return setTimeout(() => target.value = value + ' ' + digit); 175 | 176 | // If '424' + 2 177 | } else if (re.test(value + digit)) { 178 | e.preventDefault(); 179 | return setTimeout(() => target.value = value + digit + ' '); 180 | } 181 | 182 | }, 183 | 184 | formatBackCardNumber: function (e) { 185 | let target = e.currentTarget; 186 | let value = target.value; 187 | 188 | // Return unless backspacing 189 | if (e.which !== 8) { return; } 190 | 191 | // Return if focus isn't at the end of the text 192 | if ((target.selectionStart != null) && 193 | (target.selectionStart !== value.length)) { return; } 194 | 195 | // Remove the digit + trailing space 196 | if (/\d\s$/.test(value)) { 197 | e.preventDefault(); 198 | return setTimeout(() => target.value = value.replace(/\d\s$/, '')); 199 | // Remove digit if ends in space + digit 200 | } else if (/\s\d?$/.test(value)) { 201 | e.preventDefault(); 202 | return setTimeout(() => target.value = value.replace(/\d$/, '')); 203 | } 204 | }, 205 | 206 | // Format Expiry 207 | 208 | reFormatExpiry: function (e) { 209 | let target = e.currentTarget; 210 | return setTimeout(function () { 211 | let value = target.value; 212 | value = cardFormatUtils.replaceFullWidthChars(value); 213 | value = validation.formatExpiry(value); 214 | return cardFormatUtils.safeVal(value, target, e); 215 | }); 216 | }, 217 | 218 | formatExpiry: function (e) { 219 | // Only format if input is a number 220 | let digit = String.fromCharCode(e.which); 221 | if (!/^\d+$/.test(digit)) { return; } 222 | 223 | let target = e.currentTarget; 224 | let val = target.value + digit; 225 | 226 | if (/^\d$/.test(val) && !['0', '1'].includes(val)) { 227 | e.preventDefault(); 228 | return setTimeout(() => target.value = (`0${val} / `)); 229 | 230 | } else if (/^\d\d$/.test(val)) { 231 | e.preventDefault(); 232 | return setTimeout(function () { 233 | // Split for months where we have the second digit > 2 (past 12) and turn 234 | // that into (m1)(m2) => 0(m1) / (m2) 235 | let m1 = parseInt(val[0], 10); 236 | let m2 = parseInt(val[1], 10); 237 | if ((m2 > 2) && (m1 !== 0)) { 238 | return target.value = (`0${m1} / ${m2}`); 239 | } else { 240 | return target.value = (`${val} / `); 241 | } 242 | }); 243 | } 244 | }, 245 | 246 | formatForwardExpiry: function (e) { 247 | let digit = String.fromCharCode(e.which); 248 | if (!/^\d+$/.test(digit)) { return; } 249 | 250 | let target = e.currentTarget; 251 | let val = target.value; 252 | 253 | if (/^\d\d$/.test(val)) { 254 | return target.value = (`${val} / `); 255 | } 256 | }, 257 | 258 | formatForwardSlashAndSpace: function (e) { 259 | let which = String.fromCharCode(e.which); 260 | if ((which !== '/') && (which !== ' ')) { return; } 261 | 262 | let target = e.currentTarget; 263 | let val = target.value; 264 | 265 | if (/^\d$/.test(val) && (val !== '0')) { 266 | return target.value = (`0${val} / `); 267 | } 268 | }, 269 | 270 | formatBackExpiry: function (e) { 271 | let target = e.currentTarget; 272 | let value = target.value; 273 | 274 | // Return unless backspacing 275 | if (e.which !== 8) { return; } 276 | 277 | // Return if focus isn't at the end of the text 278 | if ((target.selectionStart != null) && 279 | (target.selectionStart !== value.length)) { return; } 280 | 281 | // Remove the trailing space + last digit 282 | if (/\d\s\/\s$/.test(value)) { 283 | e.preventDefault(); 284 | return setTimeout(() => target.value = value.replace(/\d\s\/\s$/, '')); 285 | } 286 | }, 287 | 288 | // Adds maxlength to Expiry field 289 | handleExpiryAttributes: function(e){ 290 | e.setAttribute('maxlength', 9); 291 | }, 292 | 293 | // Format CVC 294 | reFormatCVC: function (e) { 295 | let target = e.currentTarget; 296 | return setTimeout(function () { 297 | let value = target.value; 298 | value = cardFormatUtils.replaceFullWidthChars(value); 299 | value = value.replace(/\D/g, '').slice(0, 4); 300 | return cardFormatUtils.safeVal(value, target, e); 301 | }); 302 | }, 303 | 304 | // Restrictions 305 | restrictNumeric: function (e) { 306 | 307 | // Key event is for a browser shortcut 308 | if (e.metaKey || e.ctrlKey) { return true; } 309 | 310 | // If keycode is a space 311 | if (e.which === 32) { return false; } 312 | 313 | // If keycode is a special char (WebKit) 314 | if (e.which === 0) { return true; } 315 | 316 | // If char is a special char (Firefox) 317 | if (e.which < 33) { return true; } 318 | 319 | let input = String.fromCharCode(e.which); 320 | 321 | // Char is a number or a space 322 | return (!!/[\d\s]/.test(input)) ? true : e.preventDefault(); 323 | }, 324 | 325 | restrictCardNumber: function (e) { 326 | let target = e.currentTarget; 327 | let digit = String.fromCharCode(e.which); 328 | if (!/^\d+$/.test(digit)) { return; } 329 | if (cardFormatUtils.hasTextSelected(target)) { return; } 330 | // Restrict number of digits 331 | let value = (target.value + digit).replace(/\D/g, ''); 332 | let card = cardFormatUtils.cardFromNumber(value); 333 | 334 | if (card) { 335 | return value.length <= card.length[card.length.length - 1]; 336 | } else { 337 | // All other cards are 16 digits long 338 | return value.length <= 16; 339 | } 340 | }, 341 | 342 | restrictExpiry: function (e) { 343 | let target = e.currentTarget; 344 | let digit = String.fromCharCode(e.which); 345 | if (!/^\d+$/.test(digit)) { return; } 346 | 347 | if (cardFormatUtils.hasTextSelected(target)) { return; } 348 | 349 | let value = target.value + digit; 350 | value = value.replace(/\D/g, ''); 351 | 352 | if (value.length > 6) { return false; } 353 | }, 354 | 355 | restrictCVC: function (e) { 356 | let target = e.currentTarget; 357 | let digit = String.fromCharCode(e.which); 358 | if (!/^\d+$/.test(digit)) { return; } 359 | 360 | if (cardFormatUtils.hasTextSelected(target)) { return; } 361 | 362 | let val = target.value + digit; 363 | return val.length <= 4; 364 | }, 365 | 366 | setCardType: function (e) { 367 | 368 | let target = e.currentTarget; 369 | let val = target.value; 370 | let cardType = validation.cardType(val) || 'unknown'; 371 | 372 | if (target.className.indexOf(cardType) === -1) { 373 | 374 | let allTypes = []; 375 | for(let i in cards){ 376 | allTypes.push(cards[i].type); 377 | } 378 | 379 | target.classList.remove('unknown'); 380 | target.classList.remove('identified'); 381 | target.classList.remove(... allTypes); 382 | target.classList.add(cardType); 383 | target.dataset.cardBrand = cardType; 384 | 385 | if(cardType !== 'unknown'){ 386 | target.classList.add('identified'); 387 | } 388 | 389 | } 390 | }, 391 | 392 | __guard__: function (value, transform) { 393 | return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; 394 | } 395 | 396 | }; 397 | 398 | export default cardFormatUtils; -------------------------------------------------------------------------------- /dist/vue-credit-card-validation.esm.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vue-credit-card-validation v1.0.3 3 | * (c) 2022 Michael Wuori 4 | * Released under the MIT License. 5 | */ 6 | var cards = [ 7 | { 8 | type: 'maestro', 9 | patterns: [ 10 | 5018, 502, 503, 506, 56, 58, 639, 6220, 67, 633 11 | ], 12 | format: /(\d{1,4})/g, 13 | length: [12, 13, 14, 15, 16, 17, 18, 19], 14 | cvcLength: [3], 15 | luhn: true 16 | }, 17 | { 18 | type: 'forbrugsforeningen', 19 | patterns: [600], 20 | format: /(\d{1,4})/g, 21 | length: [16], 22 | cvcLength: [3], 23 | luhn: true 24 | }, 25 | { 26 | type: 'dankort', 27 | patterns: [5019], 28 | format: /(\d{1,4})/g, 29 | length: [16], 30 | cvcLength: [3], 31 | luhn: true 32 | }, 33 | // Credit cards 34 | { 35 | type: 'visa', 36 | patterns: [4], 37 | format: /(\d{1,4})/g, 38 | length: [13, 16], 39 | cvcLength: [3], 40 | luhn: true 41 | }, 42 | { 43 | type: 'mastercard', 44 | patterns: [ 45 | 51, 52, 53, 54, 55, 46 | 22, 23, 24, 25, 26, 27 47 | ], 48 | format: /(\d{1,4})/g, 49 | length: [16], 50 | cvcLength: [3], 51 | luhn: true 52 | }, 53 | { 54 | type: 'amex', 55 | patterns: [34, 37], 56 | format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/, 57 | length: [15, 16], 58 | cvcLength: [3, 4], 59 | luhn: true 60 | }, 61 | { 62 | type: 'dinersclub', 63 | patterns: [30, 36, 38, 39], 64 | format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/, 65 | length: [14], 66 | cvcLength: [3], 67 | luhn: true 68 | }, 69 | { 70 | type: 'discover', 71 | patterns: [60, 64, 65, 622], 72 | format: /(\d{1,4})/g, 73 | length: [16], 74 | cvcLength: [3], 75 | luhn: true 76 | }, 77 | { 78 | type: 'unionpay', 79 | patterns: [62, 88], 80 | format: /(\d{1,4})/g, 81 | length: [16, 17, 18, 19], 82 | cvcLength: [3], 83 | luhn: false 84 | }, 85 | { 86 | type: 'jcb', 87 | patterns: [35], 88 | format: /(\d{1,4})/g, 89 | length: [16], 90 | cvcLength: [3], 91 | luhn: true 92 | } 93 | ]; 94 | 95 | var validation = { 96 | 97 | cardExpiryVal: function (value) { 98 | var ref = Array.from(value.split(/[\s\/]+/, 2)); 99 | var month = ref[0]; 100 | var year = ref[1]; 101 | 102 | // Allow for year shortcut 103 | if (((year != null ? year.length : undefined) === 2) && /^\d+$/.test(year)) { 104 | var prefix = (new Date).getFullYear(); 105 | prefix = prefix.toString().slice(0, 2); 106 | year = prefix + year; 107 | } 108 | 109 | month = parseInt(month, 10); 110 | year = parseInt(year, 10); 111 | 112 | return { month: month, year: year }; 113 | }, 114 | 115 | validateCardNumber: function (num) { 116 | num = (num + '').replace(/\s+|-/g, ''); 117 | if (!/^\d+$/.test(num)) { return false; } 118 | 119 | var card = cardFormatUtils.cardFromNumber(num); 120 | if (!card) { return false; } 121 | 122 | return Array.from(card.length).includes(num.length) && 123 | ((card.luhn === false) || cardFormatUtils.luhnCheck(num)); 124 | }, 125 | 126 | validateCardExpiry: function (month, year) { 127 | 128 | if(!month){ 129 | return false; 130 | } 131 | 132 | if(!year){ 133 | var assign; 134 | ((assign = validation.cardExpiryVal(month), month = assign.month, year = assign.year)); 135 | } 136 | 137 | // Allow passing an object 138 | if ((typeof month === 'object') && 'month' in month) { 139 | var assign$1; 140 | ((assign$1 = month, month = assign$1.month, year = assign$1.year)); 141 | } 142 | 143 | if (!month || !year) { return false; } 144 | 145 | month = month.toString().trim(); 146 | year = year.toString().trim(); 147 | 148 | if (!/^\d+$/.test(month)) { return false; } 149 | if (!/^\d+$/.test(year)) { return false; } 150 | if (!(1 <= month && month <= 12)) { return false; } 151 | 152 | if (year.length === 2) { 153 | if (year < 70) { 154 | year = "20" + year; 155 | } else { 156 | year = "19" + year; 157 | } 158 | } 159 | 160 | console.log(year); 161 | 162 | if (year.length !== 4) { return false; } 163 | 164 | var expiry = new Date(year, month); 165 | var currentTime = new Date; 166 | 167 | // Months start from 0 in JavaScript 168 | expiry.setMonth(expiry.getMonth() - 1); 169 | 170 | // The cc expires at the end of the month, 171 | // so we need to make the expiry the first day 172 | // of the month after 173 | expiry.setMonth(expiry.getMonth() + 1, 1); 174 | 175 | return expiry > currentTime; 176 | }, 177 | 178 | validateCardCVC: function (cvc, type) { 179 | if(!cvc){ 180 | return false; 181 | } 182 | cvc = cvc.toString().trim(); 183 | if (!/^\d+$/.test(cvc)) { return false; } 184 | 185 | var card = cardFormatUtils.cardFromType(type); 186 | if (card != null) { 187 | // Check against a explicit card type 188 | return Array.from(card.cvcLength).includes(cvc.length); 189 | } else { 190 | // Check against all types 191 | return (cvc.length >= 3) && (cvc.length <= 4); 192 | } 193 | }, 194 | 195 | cardType: function (num) { 196 | if (!num) { return null; } 197 | return cardFormatUtils.__guard__(cardFormatUtils.cardFromNumber(num), function (x) { return x.type; }) || null; 198 | }, 199 | 200 | formatCardNumber: function (num) { 201 | 202 | num = num.toString().replace(/\D/g, ''); 203 | var card = cardFormatUtils.cardFromNumber(num); 204 | if (!card) { return num; } 205 | 206 | var upperLength = card.length[card.length.length - 1]; 207 | num = num.slice(0, upperLength); 208 | 209 | if (card.format.global) { 210 | return cardFormatUtils.__guard__(num.match(card.format), function (x) { return x.join(' '); }); 211 | } else { 212 | var groups = card.format.exec(num); 213 | if (groups == null) { return; } 214 | groups.shift(); 215 | // @TODO: Change to native filter() 216 | //groups = grep(groups, n => n); // Filter empty groups 217 | return groups.join(' '); 218 | } 219 | }, 220 | 221 | formatExpiry: function (expiry) { 222 | var parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/); 223 | if (!parts) { return ''; } 224 | 225 | var mon = parts[1] || ''; 226 | var sep = parts[2] || ''; 227 | var year = parts[3] || ''; 228 | 229 | if (year.length > 0) { 230 | sep = ' / '; 231 | 232 | } else if (sep === ' /') { 233 | mon = mon.substring(0, 1); 234 | sep = ''; 235 | 236 | } else if ((mon.length === 2) || (sep.length > 0)) { 237 | sep = ' / '; 238 | 239 | } else if ((mon.length === 1) && !['0', '1'].includes(mon)) { 240 | mon = "0" + mon; 241 | sep = ' / '; 242 | } 243 | 244 | return mon + sep + year; 245 | } 246 | }; 247 | 248 | var cardFormatUtils = { 249 | 250 | cardFromNumber : function (num) { 251 | num = (num + '').replace(/\D/g, ''); 252 | for (var i in cards) { 253 | for (var j in cards[i].patterns) { 254 | var p = cards[i].patterns[j] + ''; 255 | if (num.substr(0, p.length) === p) { return cards[i]; } 256 | } 257 | } 258 | }, 259 | 260 | cardFromType: function (type) { 261 | for (var i in cards) { if (cards[i].type === type) { return cards[i]; } } 262 | }, 263 | 264 | luhnCheck: function (num) { 265 | var odd = true; 266 | var sum = 0; 267 | 268 | var digits = (num + '').split('').reverse(); 269 | 270 | for (var i in digits) { 271 | var digit = parseInt(digits[i], 10); 272 | if (odd = !odd) { digit *= 2; } 273 | if (digit > 9) { digit -= 9; } 274 | sum += digit; 275 | } 276 | 277 | return (sum % 10) === 0; 278 | }, 279 | 280 | hasTextSelected: function (target) { 281 | // If some text is selected 282 | if ((target.selectionStart != null) && 283 | (target.selectionStart !== target.selectionEnd)) { return true; } 284 | 285 | // If some text is selected in IE 286 | if (cardFormatUtils.__guard__(typeof document !== 'undefined' && document !== null ? document.selection : undefined, function (x) { return x.createRange; }) != null) { 287 | if (document.selection.createRange().text) { return true; } 288 | } 289 | 290 | return false; 291 | }, 292 | 293 | // Private 294 | 295 | // Safe Val 296 | 297 | safeVal: function (value, target, e) { 298 | if (e.inputType === 'deleteContentBackward') { 299 | return; 300 | } 301 | var cursor; 302 | try { 303 | cursor = target.selectionStart; 304 | } catch (error) { 305 | cursor = null; 306 | } 307 | var last = target.value; 308 | target.value = value; 309 | value = target.value; 310 | if ((cursor !== null) && document.activeElement == target) { 311 | if (cursor === last.length) { cursor = target.value.length; } 312 | 313 | // This hack looks for scenarios where we are changing an input's value such 314 | // that "X| " is replaced with " |X" (where "|" is the cursor). In those 315 | // scenarios, we want " X|". 316 | // 317 | // For example: 318 | // 1. Input field has value "4444| " 319 | // 2. User types "1" 320 | // 3. Input field has value "44441| " 321 | // 4. Reformatter changes it to "4444 |1" 322 | // 5. By incrementing the cursor, we make it "4444 1|" 323 | // 324 | // This is awful, and ideally doesn't go here, but given the current design 325 | // of the system there does not appear to be a better solution. 326 | // 327 | // Note that we can't just detect when the cursor-1 is " ", because that 328 | // would incorrectly increment the cursor when backspacing, e.g. pressing 329 | // backspace in this scenario: "4444 1|234 5". 330 | if (last !== value) { 331 | var prevPair = last.slice(cursor - 1, +cursor + 1 || undefined); 332 | var currPair = target.value.slice(cursor - 1, +cursor + 1 || undefined); 333 | var digit = value[cursor]; 334 | if (/\d/.test(digit) && 335 | (prevPair === (digit + " ")) && (currPair === (" " + digit))) { cursor = cursor + 1; } 336 | } 337 | 338 | target.selectionStart = cursor; 339 | return target.selectionEnd = cursor; 340 | } 341 | }, 342 | 343 | // Replace Full-Width Chars 344 | 345 | replaceFullWidthChars: function (str) { 346 | if (str == null) { str = ''; } 347 | var fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19'; 348 | var halfWidth = '0123456789'; 349 | 350 | var value = ''; 351 | var chars = str.split(''); 352 | 353 | // Avoid using reserved word `char` 354 | for (var i in chars) { 355 | var idx = fullWidth.indexOf(chars[i]); 356 | if (idx > -1) { 357 | chars[i] = halfWidth[idx]; 358 | } 359 | value += chars[i]; 360 | } 361 | 362 | return value; 363 | }, 364 | 365 | // Format Numeric 366 | 367 | reFormatNumeric: function (e) { 368 | var target = e.currentTarget; 369 | return setTimeout(function () { 370 | var value = target.value; 371 | value = cardFormatUtils.replaceFullWidthChars(value); 372 | value = value.replace(/\D/g, ''); 373 | return cardFormatUtils.safeVal(value, target, e); 374 | }); 375 | }, 376 | 377 | // Format Card Number 378 | 379 | reFormatCardNumber: function (e) { 380 | var target = e.currentTarget; 381 | return setTimeout(function () { 382 | var value = target.value; 383 | value = cardFormatUtils.replaceFullWidthChars(value); 384 | value = validation.formatCardNumber(value); 385 | return cardFormatUtils.safeVal(value, target, e); 386 | }); 387 | }, 388 | 389 | formatCardNumber: function (e) { 390 | // Only format if input is a number 391 | var re; 392 | var digit = String.fromCharCode(e.which); 393 | if (!/^\d+$/.test(digit)) { return; } 394 | 395 | var target = e.currentTarget; 396 | var value = target.value; 397 | var card = cardFormatUtils.cardFromNumber(value + digit); 398 | var length = (value.replace(/\D/g, '') + digit); 399 | 400 | var upperLength = 16; 401 | if (card) { upperLength = card.length[card.length.length - 1]; } 402 | if (length >= upperLength) { return; } 403 | 404 | // Return if focus isn't at the end of the text 405 | if ((target.selectionStart != null) && 406 | (target.selectionStart !== value.length)) { return; } 407 | 408 | if (card && (card.type === 'amex')) { 409 | // AMEX cards are formatted differently 410 | re = /^(\d{4}|\d{4}\s\d{6})$/; 411 | } else { 412 | re = /(?:^|\s)(\d{4})$/; 413 | } 414 | 415 | // If '4242' + 4 416 | if (re.test(value + digit)) { 417 | e.preventDefault(); 418 | return setTimeout(function () { return target.value = value + ' ' + digit; }); 419 | 420 | // If '424' + 2 421 | } else if (re.test(value + digit)) { 422 | e.preventDefault(); 423 | return setTimeout(function () { return target.value = value + digit + ' '; }); 424 | } 425 | 426 | }, 427 | 428 | formatBackCardNumber: function (e) { 429 | var target = e.currentTarget; 430 | var value = target.value; 431 | 432 | // Return unless backspacing 433 | if (e.which !== 8) { return; } 434 | 435 | // Return if focus isn't at the end of the text 436 | if ((target.selectionStart != null) && 437 | (target.selectionStart !== value.length)) { return; } 438 | 439 | // Remove the digit + trailing space 440 | if (/\d\s$/.test(value)) { 441 | e.preventDefault(); 442 | return setTimeout(function () { return target.value = value.replace(/\d\s$/, ''); }); 443 | // Remove digit if ends in space + digit 444 | } else if (/\s\d?$/.test(value)) { 445 | e.preventDefault(); 446 | return setTimeout(function () { return target.value = value.replace(/\d$/, ''); }); 447 | } 448 | }, 449 | 450 | // Format Expiry 451 | 452 | reFormatExpiry: function (e) { 453 | var target = e.currentTarget; 454 | return setTimeout(function () { 455 | var value = target.value; 456 | value = cardFormatUtils.replaceFullWidthChars(value); 457 | value = validation.formatExpiry(value); 458 | return cardFormatUtils.safeVal(value, target, e); 459 | }); 460 | }, 461 | 462 | formatExpiry: function (e) { 463 | // Only format if input is a number 464 | var digit = String.fromCharCode(e.which); 465 | if (!/^\d+$/.test(digit)) { return; } 466 | 467 | var target = e.currentTarget; 468 | var val = target.value + digit; 469 | 470 | if (/^\d$/.test(val) && !['0', '1'].includes(val)) { 471 | e.preventDefault(); 472 | return setTimeout(function () { return target.value = (("0" + val + " / ")); }); 473 | 474 | } else if (/^\d\d$/.test(val)) { 475 | e.preventDefault(); 476 | return setTimeout(function () { 477 | // Split for months where we have the second digit > 2 (past 12) and turn 478 | // that into (m1)(m2) => 0(m1) / (m2) 479 | var m1 = parseInt(val[0], 10); 480 | var m2 = parseInt(val[1], 10); 481 | if ((m2 > 2) && (m1 !== 0)) { 482 | return target.value = (("0" + m1 + " / " + m2)); 483 | } else { 484 | return target.value = ((val + " / ")); 485 | } 486 | }); 487 | } 488 | }, 489 | 490 | formatForwardExpiry: function (e) { 491 | var digit = String.fromCharCode(e.which); 492 | if (!/^\d+$/.test(digit)) { return; } 493 | 494 | var target = e.currentTarget; 495 | var val = target.value; 496 | 497 | if (/^\d\d$/.test(val)) { 498 | return target.value = ((val + " / ")); 499 | } 500 | }, 501 | 502 | formatForwardSlashAndSpace: function (e) { 503 | var which = String.fromCharCode(e.which); 504 | if ((which !== '/') && (which !== ' ')) { return; } 505 | 506 | var target = e.currentTarget; 507 | var val = target.value; 508 | 509 | if (/^\d$/.test(val) && (val !== '0')) { 510 | return target.value = (("0" + val + " / ")); 511 | } 512 | }, 513 | 514 | formatBackExpiry: function (e) { 515 | var target = e.currentTarget; 516 | var value = target.value; 517 | 518 | // Return unless backspacing 519 | if (e.which !== 8) { return; } 520 | 521 | // Return if focus isn't at the end of the text 522 | if ((target.selectionStart != null) && 523 | (target.selectionStart !== value.length)) { return; } 524 | 525 | // Remove the trailing space + last digit 526 | if (/\d\s\/\s$/.test(value)) { 527 | e.preventDefault(); 528 | return setTimeout(function () { return target.value = value.replace(/\d\s\/\s$/, ''); }); 529 | } 530 | }, 531 | 532 | // Adds maxlength to Expiry field 533 | handleExpiryAttributes: function(e){ 534 | e.setAttribute('maxlength', 9); 535 | }, 536 | 537 | // Format CVC 538 | reFormatCVC: function (e) { 539 | var target = e.currentTarget; 540 | return setTimeout(function () { 541 | var value = target.value; 542 | value = cardFormatUtils.replaceFullWidthChars(value); 543 | value = value.replace(/\D/g, '').slice(0, 4); 544 | return cardFormatUtils.safeVal(value, target, e); 545 | }); 546 | }, 547 | 548 | // Restrictions 549 | restrictNumeric: function (e) { 550 | 551 | // Key event is for a browser shortcut 552 | if (e.metaKey || e.ctrlKey) { return true; } 553 | 554 | // If keycode is a space 555 | if (e.which === 32) { return false; } 556 | 557 | // If keycode is a special char (WebKit) 558 | if (e.which === 0) { return true; } 559 | 560 | // If char is a special char (Firefox) 561 | if (e.which < 33) { return true; } 562 | 563 | var input = String.fromCharCode(e.which); 564 | 565 | // Char is a number or a space 566 | return (!!/[\d\s]/.test(input)) ? true : e.preventDefault(); 567 | }, 568 | 569 | restrictCardNumber: function (e) { 570 | var target = e.currentTarget; 571 | var digit = String.fromCharCode(e.which); 572 | if (!/^\d+$/.test(digit)) { return; } 573 | if (cardFormatUtils.hasTextSelected(target)) { return; } 574 | // Restrict number of digits 575 | var value = (target.value + digit).replace(/\D/g, ''); 576 | var card = cardFormatUtils.cardFromNumber(value); 577 | 578 | if (card) { 579 | return value.length <= card.length[card.length.length - 1]; 580 | } else { 581 | // All other cards are 16 digits long 582 | return value.length <= 16; 583 | } 584 | }, 585 | 586 | restrictExpiry: function (e) { 587 | var target = e.currentTarget; 588 | var digit = String.fromCharCode(e.which); 589 | if (!/^\d+$/.test(digit)) { return; } 590 | 591 | if (cardFormatUtils.hasTextSelected(target)) { return; } 592 | 593 | var value = target.value + digit; 594 | value = value.replace(/\D/g, ''); 595 | 596 | if (value.length > 6) { return false; } 597 | }, 598 | 599 | restrictCVC: function (e) { 600 | var target = e.currentTarget; 601 | var digit = String.fromCharCode(e.which); 602 | if (!/^\d+$/.test(digit)) { return; } 603 | 604 | if (cardFormatUtils.hasTextSelected(target)) { return; } 605 | 606 | var val = target.value + digit; 607 | return val.length <= 4; 608 | }, 609 | 610 | setCardType: function (e) { 611 | 612 | var target = e.currentTarget; 613 | var val = target.value; 614 | var cardType = validation.cardType(val) || 'unknown'; 615 | 616 | if (target.className.indexOf(cardType) === -1) { 617 | 618 | var allTypes = []; 619 | for(var i in cards){ 620 | allTypes.push(cards[i].type); 621 | } 622 | 623 | target.classList.remove('unknown'); 624 | target.classList.remove('identified'); 625 | (ref = target.classList).remove.apply(ref, allTypes); 626 | target.classList.add(cardType); 627 | target.dataset.cardBrand = cardType; 628 | 629 | if(cardType !== 'unknown'){ 630 | target.classList.add('identified'); 631 | } 632 | 633 | } 634 | var ref; 635 | }, 636 | 637 | __guard__: function (value, transform) { 638 | return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; 639 | } 640 | 641 | }; 642 | 643 | var format = { 644 | 645 | validateCardNumber: validation.validateCardNumber, 646 | validateCardCVC: validation.validateCardCVC, 647 | validateCardExpiry: validation.validateCardExpiry, 648 | 649 | setCardType: function(el) { 650 | cardFormatUtils.setCardType(el); 651 | setTimeout(function(){ 652 | el.currentTarget.dispatchEvent(new Event('keyup')); 653 | el.currentTarget.dispatchEvent(new Event('change')); 654 | }, 100); 655 | }, 656 | 657 | formatCardCVC: function (el) { 658 | el.addEventListener('keypress', cardFormatUtils.restrictNumeric); 659 | el.addEventListener('keypress', cardFormatUtils.restrictCVC); 660 | el.addEventListener('paste', cardFormatUtils.reFormatCVC); 661 | el.addEventListener('change', cardFormatUtils.reFormatCVC); 662 | el.addEventListener('input', cardFormatUtils.reFormatCVC); 663 | return this; 664 | }, 665 | 666 | formatCardExpiry: function (el) { 667 | cardFormatUtils.handleExpiryAttributes(el); 668 | el.addEventListener('keypress', cardFormatUtils.restrictNumeric); 669 | el.addEventListener('keypress', cardFormatUtils.formatExpiry); 670 | el.addEventListener('keypress', cardFormatUtils.formatForwardSlashAndSpace); 671 | el.addEventListener('keypress', cardFormatUtils.formatForwardExpiry); 672 | el.addEventListener('keydown', cardFormatUtils.formatBackExpiry); 673 | el.addEventListener('change', cardFormatUtils.reFormatExpiry); 674 | el.addEventListener('input', cardFormatUtils.reFormatExpiry); 675 | el.addEventListener('blur', cardFormatUtils.reFormatExpiry); 676 | return this; 677 | }, 678 | 679 | formatCardNumber: function (el) { 680 | el.maxLength = 19; 681 | el.addEventListener('keypress', cardFormatUtils.restrictNumeric); 682 | el.addEventListener('keypress', cardFormatUtils.restrictCardNumber); 683 | el.addEventListener('keypress', cardFormatUtils.formatCardNumber); 684 | el.addEventListener('keydown', cardFormatUtils.formatBackCardNumber); 685 | el.addEventListener('keyup', cardFormatUtils.setCardType); 686 | el.addEventListener('paste', cardFormatUtils.reFormatCardNumber); 687 | el.addEventListener('change', cardFormatUtils.reFormatCardNumber); 688 | el.addEventListener('input', cardFormatUtils.reFormatCardNumber); 689 | el.addEventListener('input', cardFormatUtils.setCardType); 690 | return this; 691 | }, 692 | 693 | restrictNumeric: function (el) { 694 | el.addEventListener('keypress', cardFormatUtils.restrictNumeric); 695 | el.addEventListener('paste', cardFormatUtils.restrictNumeric); 696 | el.addEventListener('change', cardFormatUtils.restrictNumeric); 697 | el.addEventListener('input', cardFormatUtils.restrictNumeric); 698 | return this; 699 | } 700 | }; 701 | 702 | var VueCardFormat = { 703 | install: function install(vue, opts) { 704 | // provide plugin to Vue 705 | vue.config.globalProperties.$cardFormat = format; 706 | // provide directive 707 | vue.directive('cardformat', { 708 | beforeMount: function beforeMount(el, binding, vnode) { 709 | // see if el is an input 710 | if (el.nodeName.toLowerCase() !== 'input'){ 711 | el = el.querySelector('input'); 712 | } 713 | // call format function from prop 714 | var method = Object.keys(format).find(function (key) { return key.toLowerCase() === binding.arg.toLowerCase(); }); 715 | var keyupEvent = new Event('keyup'); 716 | format[method](el, vnode); 717 | // update cardBrand value if available 718 | if (method == 'formatCardNumber' && typeof binding.instance.cardBrand !== 'undefined'){ 719 | el.addEventListener('keyup', function () { 720 | if (el.dataset.cardBrand) { 721 | binding.instance.cardBrand = el.dataset.cardBrand; 722 | } 723 | }); 724 | el.addEventListener('paste', function () { 725 | setTimeout(function () { 726 | el.dispatchEvent(keyupEvent); 727 | },10); 728 | }); 729 | } 730 | } 731 | }); 732 | } 733 | }; 734 | 735 | export default VueCardFormat; 736 | -------------------------------------------------------------------------------- /dist/vue-credit-card-validation.common.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vue-credit-card-validation v1.0.3 3 | * (c) 2022 Michael Wuori 4 | * Released under the MIT License. 5 | */ 6 | 'use strict'; 7 | 8 | var cards = [ 9 | { 10 | type: 'maestro', 11 | patterns: [ 12 | 5018, 502, 503, 506, 56, 58, 639, 6220, 67, 633 13 | ], 14 | format: /(\d{1,4})/g, 15 | length: [12, 13, 14, 15, 16, 17, 18, 19], 16 | cvcLength: [3], 17 | luhn: true 18 | }, 19 | { 20 | type: 'forbrugsforeningen', 21 | patterns: [600], 22 | format: /(\d{1,4})/g, 23 | length: [16], 24 | cvcLength: [3], 25 | luhn: true 26 | }, 27 | { 28 | type: 'dankort', 29 | patterns: [5019], 30 | format: /(\d{1,4})/g, 31 | length: [16], 32 | cvcLength: [3], 33 | luhn: true 34 | }, 35 | // Credit cards 36 | { 37 | type: 'visa', 38 | patterns: [4], 39 | format: /(\d{1,4})/g, 40 | length: [13, 16], 41 | cvcLength: [3], 42 | luhn: true 43 | }, 44 | { 45 | type: 'mastercard', 46 | patterns: [ 47 | 51, 52, 53, 54, 55, 48 | 22, 23, 24, 25, 26, 27 49 | ], 50 | format: /(\d{1,4})/g, 51 | length: [16], 52 | cvcLength: [3], 53 | luhn: true 54 | }, 55 | { 56 | type: 'amex', 57 | patterns: [34, 37], 58 | format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/, 59 | length: [15, 16], 60 | cvcLength: [3, 4], 61 | luhn: true 62 | }, 63 | { 64 | type: 'dinersclub', 65 | patterns: [30, 36, 38, 39], 66 | format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/, 67 | length: [14], 68 | cvcLength: [3], 69 | luhn: true 70 | }, 71 | { 72 | type: 'discover', 73 | patterns: [60, 64, 65, 622], 74 | format: /(\d{1,4})/g, 75 | length: [16], 76 | cvcLength: [3], 77 | luhn: true 78 | }, 79 | { 80 | type: 'unionpay', 81 | patterns: [62, 88], 82 | format: /(\d{1,4})/g, 83 | length: [16, 17, 18, 19], 84 | cvcLength: [3], 85 | luhn: false 86 | }, 87 | { 88 | type: 'jcb', 89 | patterns: [35], 90 | format: /(\d{1,4})/g, 91 | length: [16], 92 | cvcLength: [3], 93 | luhn: true 94 | } 95 | ]; 96 | 97 | var validation = { 98 | 99 | cardExpiryVal: function (value) { 100 | var ref = Array.from(value.split(/[\s\/]+/, 2)); 101 | var month = ref[0]; 102 | var year = ref[1]; 103 | 104 | // Allow for year shortcut 105 | if (((year != null ? year.length : undefined) === 2) && /^\d+$/.test(year)) { 106 | var prefix = (new Date).getFullYear(); 107 | prefix = prefix.toString().slice(0, 2); 108 | year = prefix + year; 109 | } 110 | 111 | month = parseInt(month, 10); 112 | year = parseInt(year, 10); 113 | 114 | return { month: month, year: year }; 115 | }, 116 | 117 | validateCardNumber: function (num) { 118 | num = (num + '').replace(/\s+|-/g, ''); 119 | if (!/^\d+$/.test(num)) { return false; } 120 | 121 | var card = cardFormatUtils.cardFromNumber(num); 122 | if (!card) { return false; } 123 | 124 | return Array.from(card.length).includes(num.length) && 125 | ((card.luhn === false) || cardFormatUtils.luhnCheck(num)); 126 | }, 127 | 128 | validateCardExpiry: function (month, year) { 129 | 130 | if(!month){ 131 | return false; 132 | } 133 | 134 | if(!year){ 135 | var assign; 136 | ((assign = validation.cardExpiryVal(month), month = assign.month, year = assign.year)); 137 | } 138 | 139 | // Allow passing an object 140 | if ((typeof month === 'object') && 'month' in month) { 141 | var assign$1; 142 | ((assign$1 = month, month = assign$1.month, year = assign$1.year)); 143 | } 144 | 145 | if (!month || !year) { return false; } 146 | 147 | month = month.toString().trim(); 148 | year = year.toString().trim(); 149 | 150 | if (!/^\d+$/.test(month)) { return false; } 151 | if (!/^\d+$/.test(year)) { return false; } 152 | if (!(1 <= month && month <= 12)) { return false; } 153 | 154 | if (year.length === 2) { 155 | if (year < 70) { 156 | year = "20" + year; 157 | } else { 158 | year = "19" + year; 159 | } 160 | } 161 | 162 | console.log(year); 163 | 164 | if (year.length !== 4) { return false; } 165 | 166 | var expiry = new Date(year, month); 167 | var currentTime = new Date; 168 | 169 | // Months start from 0 in JavaScript 170 | expiry.setMonth(expiry.getMonth() - 1); 171 | 172 | // The cc expires at the end of the month, 173 | // so we need to make the expiry the first day 174 | // of the month after 175 | expiry.setMonth(expiry.getMonth() + 1, 1); 176 | 177 | return expiry > currentTime; 178 | }, 179 | 180 | validateCardCVC: function (cvc, type) { 181 | if(!cvc){ 182 | return false; 183 | } 184 | cvc = cvc.toString().trim(); 185 | if (!/^\d+$/.test(cvc)) { return false; } 186 | 187 | var card = cardFormatUtils.cardFromType(type); 188 | if (card != null) { 189 | // Check against a explicit card type 190 | return Array.from(card.cvcLength).includes(cvc.length); 191 | } else { 192 | // Check against all types 193 | return (cvc.length >= 3) && (cvc.length <= 4); 194 | } 195 | }, 196 | 197 | cardType: function (num) { 198 | if (!num) { return null; } 199 | return cardFormatUtils.__guard__(cardFormatUtils.cardFromNumber(num), function (x) { return x.type; }) || null; 200 | }, 201 | 202 | formatCardNumber: function (num) { 203 | 204 | num = num.toString().replace(/\D/g, ''); 205 | var card = cardFormatUtils.cardFromNumber(num); 206 | if (!card) { return num; } 207 | 208 | var upperLength = card.length[card.length.length - 1]; 209 | num = num.slice(0, upperLength); 210 | 211 | if (card.format.global) { 212 | return cardFormatUtils.__guard__(num.match(card.format), function (x) { return x.join(' '); }); 213 | } else { 214 | var groups = card.format.exec(num); 215 | if (groups == null) { return; } 216 | groups.shift(); 217 | // @TODO: Change to native filter() 218 | //groups = grep(groups, n => n); // Filter empty groups 219 | return groups.join(' '); 220 | } 221 | }, 222 | 223 | formatExpiry: function (expiry) { 224 | var parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/); 225 | if (!parts) { return ''; } 226 | 227 | var mon = parts[1] || ''; 228 | var sep = parts[2] || ''; 229 | var year = parts[3] || ''; 230 | 231 | if (year.length > 0) { 232 | sep = ' / '; 233 | 234 | } else if (sep === ' /') { 235 | mon = mon.substring(0, 1); 236 | sep = ''; 237 | 238 | } else if ((mon.length === 2) || (sep.length > 0)) { 239 | sep = ' / '; 240 | 241 | } else if ((mon.length === 1) && !['0', '1'].includes(mon)) { 242 | mon = "0" + mon; 243 | sep = ' / '; 244 | } 245 | 246 | return mon + sep + year; 247 | } 248 | }; 249 | 250 | var cardFormatUtils = { 251 | 252 | cardFromNumber : function (num) { 253 | num = (num + '').replace(/\D/g, ''); 254 | for (var i in cards) { 255 | for (var j in cards[i].patterns) { 256 | var p = cards[i].patterns[j] + ''; 257 | if (num.substr(0, p.length) === p) { return cards[i]; } 258 | } 259 | } 260 | }, 261 | 262 | cardFromType: function (type) { 263 | for (var i in cards) { if (cards[i].type === type) { return cards[i]; } } 264 | }, 265 | 266 | luhnCheck: function (num) { 267 | var odd = true; 268 | var sum = 0; 269 | 270 | var digits = (num + '').split('').reverse(); 271 | 272 | for (var i in digits) { 273 | var digit = parseInt(digits[i], 10); 274 | if (odd = !odd) { digit *= 2; } 275 | if (digit > 9) { digit -= 9; } 276 | sum += digit; 277 | } 278 | 279 | return (sum % 10) === 0; 280 | }, 281 | 282 | hasTextSelected: function (target) { 283 | // If some text is selected 284 | if ((target.selectionStart != null) && 285 | (target.selectionStart !== target.selectionEnd)) { return true; } 286 | 287 | // If some text is selected in IE 288 | if (cardFormatUtils.__guard__(typeof document !== 'undefined' && document !== null ? document.selection : undefined, function (x) { return x.createRange; }) != null) { 289 | if (document.selection.createRange().text) { return true; } 290 | } 291 | 292 | return false; 293 | }, 294 | 295 | // Private 296 | 297 | // Safe Val 298 | 299 | safeVal: function (value, target, e) { 300 | if (e.inputType === 'deleteContentBackward') { 301 | return; 302 | } 303 | var cursor; 304 | try { 305 | cursor = target.selectionStart; 306 | } catch (error) { 307 | cursor = null; 308 | } 309 | var last = target.value; 310 | target.value = value; 311 | value = target.value; 312 | if ((cursor !== null) && document.activeElement == target) { 313 | if (cursor === last.length) { cursor = target.value.length; } 314 | 315 | // This hack looks for scenarios where we are changing an input's value such 316 | // that "X| " is replaced with " |X" (where "|" is the cursor). In those 317 | // scenarios, we want " X|". 318 | // 319 | // For example: 320 | // 1. Input field has value "4444| " 321 | // 2. User types "1" 322 | // 3. Input field has value "44441| " 323 | // 4. Reformatter changes it to "4444 |1" 324 | // 5. By incrementing the cursor, we make it "4444 1|" 325 | // 326 | // This is awful, and ideally doesn't go here, but given the current design 327 | // of the system there does not appear to be a better solution. 328 | // 329 | // Note that we can't just detect when the cursor-1 is " ", because that 330 | // would incorrectly increment the cursor when backspacing, e.g. pressing 331 | // backspace in this scenario: "4444 1|234 5". 332 | if (last !== value) { 333 | var prevPair = last.slice(cursor - 1, +cursor + 1 || undefined); 334 | var currPair = target.value.slice(cursor - 1, +cursor + 1 || undefined); 335 | var digit = value[cursor]; 336 | if (/\d/.test(digit) && 337 | (prevPair === (digit + " ")) && (currPair === (" " + digit))) { cursor = cursor + 1; } 338 | } 339 | 340 | target.selectionStart = cursor; 341 | return target.selectionEnd = cursor; 342 | } 343 | }, 344 | 345 | // Replace Full-Width Chars 346 | 347 | replaceFullWidthChars: function (str) { 348 | if (str == null) { str = ''; } 349 | var fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19'; 350 | var halfWidth = '0123456789'; 351 | 352 | var value = ''; 353 | var chars = str.split(''); 354 | 355 | // Avoid using reserved word `char` 356 | for (var i in chars) { 357 | var idx = fullWidth.indexOf(chars[i]); 358 | if (idx > -1) { 359 | chars[i] = halfWidth[idx]; 360 | } 361 | value += chars[i]; 362 | } 363 | 364 | return value; 365 | }, 366 | 367 | // Format Numeric 368 | 369 | reFormatNumeric: function (e) { 370 | var target = e.currentTarget; 371 | return setTimeout(function () { 372 | var value = target.value; 373 | value = cardFormatUtils.replaceFullWidthChars(value); 374 | value = value.replace(/\D/g, ''); 375 | return cardFormatUtils.safeVal(value, target, e); 376 | }); 377 | }, 378 | 379 | // Format Card Number 380 | 381 | reFormatCardNumber: function (e) { 382 | var target = e.currentTarget; 383 | return setTimeout(function () { 384 | var value = target.value; 385 | value = cardFormatUtils.replaceFullWidthChars(value); 386 | value = validation.formatCardNumber(value); 387 | return cardFormatUtils.safeVal(value, target, e); 388 | }); 389 | }, 390 | 391 | formatCardNumber: function (e) { 392 | // Only format if input is a number 393 | var re; 394 | var digit = String.fromCharCode(e.which); 395 | if (!/^\d+$/.test(digit)) { return; } 396 | 397 | var target = e.currentTarget; 398 | var value = target.value; 399 | var card = cardFormatUtils.cardFromNumber(value + digit); 400 | var length = (value.replace(/\D/g, '') + digit); 401 | 402 | var upperLength = 16; 403 | if (card) { upperLength = card.length[card.length.length - 1]; } 404 | if (length >= upperLength) { return; } 405 | 406 | // Return if focus isn't at the end of the text 407 | if ((target.selectionStart != null) && 408 | (target.selectionStart !== value.length)) { return; } 409 | 410 | if (card && (card.type === 'amex')) { 411 | // AMEX cards are formatted differently 412 | re = /^(\d{4}|\d{4}\s\d{6})$/; 413 | } else { 414 | re = /(?:^|\s)(\d{4})$/; 415 | } 416 | 417 | // If '4242' + 4 418 | if (re.test(value + digit)) { 419 | e.preventDefault(); 420 | return setTimeout(function () { return target.value = value + ' ' + digit; }); 421 | 422 | // If '424' + 2 423 | } else if (re.test(value + digit)) { 424 | e.preventDefault(); 425 | return setTimeout(function () { return target.value = value + digit + ' '; }); 426 | } 427 | 428 | }, 429 | 430 | formatBackCardNumber: function (e) { 431 | var target = e.currentTarget; 432 | var value = target.value; 433 | 434 | // Return unless backspacing 435 | if (e.which !== 8) { return; } 436 | 437 | // Return if focus isn't at the end of the text 438 | if ((target.selectionStart != null) && 439 | (target.selectionStart !== value.length)) { return; } 440 | 441 | // Remove the digit + trailing space 442 | if (/\d\s$/.test(value)) { 443 | e.preventDefault(); 444 | return setTimeout(function () { return target.value = value.replace(/\d\s$/, ''); }); 445 | // Remove digit if ends in space + digit 446 | } else if (/\s\d?$/.test(value)) { 447 | e.preventDefault(); 448 | return setTimeout(function () { return target.value = value.replace(/\d$/, ''); }); 449 | } 450 | }, 451 | 452 | // Format Expiry 453 | 454 | reFormatExpiry: function (e) { 455 | var target = e.currentTarget; 456 | return setTimeout(function () { 457 | var value = target.value; 458 | value = cardFormatUtils.replaceFullWidthChars(value); 459 | value = validation.formatExpiry(value); 460 | return cardFormatUtils.safeVal(value, target, e); 461 | }); 462 | }, 463 | 464 | formatExpiry: function (e) { 465 | // Only format if input is a number 466 | var digit = String.fromCharCode(e.which); 467 | if (!/^\d+$/.test(digit)) { return; } 468 | 469 | var target = e.currentTarget; 470 | var val = target.value + digit; 471 | 472 | if (/^\d$/.test(val) && !['0', '1'].includes(val)) { 473 | e.preventDefault(); 474 | return setTimeout(function () { return target.value = (("0" + val + " / ")); }); 475 | 476 | } else if (/^\d\d$/.test(val)) { 477 | e.preventDefault(); 478 | return setTimeout(function () { 479 | // Split for months where we have the second digit > 2 (past 12) and turn 480 | // that into (m1)(m2) => 0(m1) / (m2) 481 | var m1 = parseInt(val[0], 10); 482 | var m2 = parseInt(val[1], 10); 483 | if ((m2 > 2) && (m1 !== 0)) { 484 | return target.value = (("0" + m1 + " / " + m2)); 485 | } else { 486 | return target.value = ((val + " / ")); 487 | } 488 | }); 489 | } 490 | }, 491 | 492 | formatForwardExpiry: function (e) { 493 | var digit = String.fromCharCode(e.which); 494 | if (!/^\d+$/.test(digit)) { return; } 495 | 496 | var target = e.currentTarget; 497 | var val = target.value; 498 | 499 | if (/^\d\d$/.test(val)) { 500 | return target.value = ((val + " / ")); 501 | } 502 | }, 503 | 504 | formatForwardSlashAndSpace: function (e) { 505 | var which = String.fromCharCode(e.which); 506 | if ((which !== '/') && (which !== ' ')) { return; } 507 | 508 | var target = e.currentTarget; 509 | var val = target.value; 510 | 511 | if (/^\d$/.test(val) && (val !== '0')) { 512 | return target.value = (("0" + val + " / ")); 513 | } 514 | }, 515 | 516 | formatBackExpiry: function (e) { 517 | var target = e.currentTarget; 518 | var value = target.value; 519 | 520 | // Return unless backspacing 521 | if (e.which !== 8) { return; } 522 | 523 | // Return if focus isn't at the end of the text 524 | if ((target.selectionStart != null) && 525 | (target.selectionStart !== value.length)) { return; } 526 | 527 | // Remove the trailing space + last digit 528 | if (/\d\s\/\s$/.test(value)) { 529 | e.preventDefault(); 530 | return setTimeout(function () { return target.value = value.replace(/\d\s\/\s$/, ''); }); 531 | } 532 | }, 533 | 534 | // Adds maxlength to Expiry field 535 | handleExpiryAttributes: function(e){ 536 | e.setAttribute('maxlength', 9); 537 | }, 538 | 539 | // Format CVC 540 | reFormatCVC: function (e) { 541 | var target = e.currentTarget; 542 | return setTimeout(function () { 543 | var value = target.value; 544 | value = cardFormatUtils.replaceFullWidthChars(value); 545 | value = value.replace(/\D/g, '').slice(0, 4); 546 | return cardFormatUtils.safeVal(value, target, e); 547 | }); 548 | }, 549 | 550 | // Restrictions 551 | restrictNumeric: function (e) { 552 | 553 | // Key event is for a browser shortcut 554 | if (e.metaKey || e.ctrlKey) { return true; } 555 | 556 | // If keycode is a space 557 | if (e.which === 32) { return false; } 558 | 559 | // If keycode is a special char (WebKit) 560 | if (e.which === 0) { return true; } 561 | 562 | // If char is a special char (Firefox) 563 | if (e.which < 33) { return true; } 564 | 565 | var input = String.fromCharCode(e.which); 566 | 567 | // Char is a number or a space 568 | return (!!/[\d\s]/.test(input)) ? true : e.preventDefault(); 569 | }, 570 | 571 | restrictCardNumber: function (e) { 572 | var target = e.currentTarget; 573 | var digit = String.fromCharCode(e.which); 574 | if (!/^\d+$/.test(digit)) { return; } 575 | if (cardFormatUtils.hasTextSelected(target)) { return; } 576 | // Restrict number of digits 577 | var value = (target.value + digit).replace(/\D/g, ''); 578 | var card = cardFormatUtils.cardFromNumber(value); 579 | 580 | if (card) { 581 | return value.length <= card.length[card.length.length - 1]; 582 | } else { 583 | // All other cards are 16 digits long 584 | return value.length <= 16; 585 | } 586 | }, 587 | 588 | restrictExpiry: function (e) { 589 | var target = e.currentTarget; 590 | var digit = String.fromCharCode(e.which); 591 | if (!/^\d+$/.test(digit)) { return; } 592 | 593 | if (cardFormatUtils.hasTextSelected(target)) { return; } 594 | 595 | var value = target.value + digit; 596 | value = value.replace(/\D/g, ''); 597 | 598 | if (value.length > 6) { return false; } 599 | }, 600 | 601 | restrictCVC: function (e) { 602 | var target = e.currentTarget; 603 | var digit = String.fromCharCode(e.which); 604 | if (!/^\d+$/.test(digit)) { return; } 605 | 606 | if (cardFormatUtils.hasTextSelected(target)) { return; } 607 | 608 | var val = target.value + digit; 609 | return val.length <= 4; 610 | }, 611 | 612 | setCardType: function (e) { 613 | 614 | var target = e.currentTarget; 615 | var val = target.value; 616 | var cardType = validation.cardType(val) || 'unknown'; 617 | 618 | if (target.className.indexOf(cardType) === -1) { 619 | 620 | var allTypes = []; 621 | for(var i in cards){ 622 | allTypes.push(cards[i].type); 623 | } 624 | 625 | target.classList.remove('unknown'); 626 | target.classList.remove('identified'); 627 | (ref = target.classList).remove.apply(ref, allTypes); 628 | target.classList.add(cardType); 629 | target.dataset.cardBrand = cardType; 630 | 631 | if(cardType !== 'unknown'){ 632 | target.classList.add('identified'); 633 | } 634 | 635 | } 636 | var ref; 637 | }, 638 | 639 | __guard__: function (value, transform) { 640 | return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; 641 | } 642 | 643 | }; 644 | 645 | var format = { 646 | 647 | validateCardNumber: validation.validateCardNumber, 648 | validateCardCVC: validation.validateCardCVC, 649 | validateCardExpiry: validation.validateCardExpiry, 650 | 651 | setCardType: function(el) { 652 | cardFormatUtils.setCardType(el); 653 | setTimeout(function(){ 654 | el.currentTarget.dispatchEvent(new Event('keyup')); 655 | el.currentTarget.dispatchEvent(new Event('change')); 656 | }, 100); 657 | }, 658 | 659 | formatCardCVC: function (el) { 660 | el.addEventListener('keypress', cardFormatUtils.restrictNumeric); 661 | el.addEventListener('keypress', cardFormatUtils.restrictCVC); 662 | el.addEventListener('paste', cardFormatUtils.reFormatCVC); 663 | el.addEventListener('change', cardFormatUtils.reFormatCVC); 664 | el.addEventListener('input', cardFormatUtils.reFormatCVC); 665 | return this; 666 | }, 667 | 668 | formatCardExpiry: function (el) { 669 | cardFormatUtils.handleExpiryAttributes(el); 670 | el.addEventListener('keypress', cardFormatUtils.restrictNumeric); 671 | el.addEventListener('keypress', cardFormatUtils.formatExpiry); 672 | el.addEventListener('keypress', cardFormatUtils.formatForwardSlashAndSpace); 673 | el.addEventListener('keypress', cardFormatUtils.formatForwardExpiry); 674 | el.addEventListener('keydown', cardFormatUtils.formatBackExpiry); 675 | el.addEventListener('change', cardFormatUtils.reFormatExpiry); 676 | el.addEventListener('input', cardFormatUtils.reFormatExpiry); 677 | el.addEventListener('blur', cardFormatUtils.reFormatExpiry); 678 | return this; 679 | }, 680 | 681 | formatCardNumber: function (el) { 682 | el.maxLength = 19; 683 | el.addEventListener('keypress', cardFormatUtils.restrictNumeric); 684 | el.addEventListener('keypress', cardFormatUtils.restrictCardNumber); 685 | el.addEventListener('keypress', cardFormatUtils.formatCardNumber); 686 | el.addEventListener('keydown', cardFormatUtils.formatBackCardNumber); 687 | el.addEventListener('keyup', cardFormatUtils.setCardType); 688 | el.addEventListener('paste', cardFormatUtils.reFormatCardNumber); 689 | el.addEventListener('change', cardFormatUtils.reFormatCardNumber); 690 | el.addEventListener('input', cardFormatUtils.reFormatCardNumber); 691 | el.addEventListener('input', cardFormatUtils.setCardType); 692 | return this; 693 | }, 694 | 695 | restrictNumeric: function (el) { 696 | el.addEventListener('keypress', cardFormatUtils.restrictNumeric); 697 | el.addEventListener('paste', cardFormatUtils.restrictNumeric); 698 | el.addEventListener('change', cardFormatUtils.restrictNumeric); 699 | el.addEventListener('input', cardFormatUtils.restrictNumeric); 700 | return this; 701 | } 702 | }; 703 | 704 | var VueCardFormat = { 705 | install: function install(vue, opts) { 706 | // provide plugin to Vue 707 | vue.config.globalProperties.$cardFormat = format; 708 | // provide directive 709 | vue.directive('cardformat', { 710 | beforeMount: function beforeMount(el, binding, vnode) { 711 | // see if el is an input 712 | if (el.nodeName.toLowerCase() !== 'input'){ 713 | el = el.querySelector('input'); 714 | } 715 | // call format function from prop 716 | var method = Object.keys(format).find(function (key) { return key.toLowerCase() === binding.arg.toLowerCase(); }); 717 | var keyupEvent = new Event('keyup'); 718 | format[method](el, vnode); 719 | // update cardBrand value if available 720 | if (method == 'formatCardNumber' && typeof binding.instance.cardBrand !== 'undefined'){ 721 | el.addEventListener('keyup', function () { 722 | if (el.dataset.cardBrand) { 723 | binding.instance.cardBrand = el.dataset.cardBrand; 724 | } 725 | }); 726 | el.addEventListener('paste', function () { 727 | setTimeout(function () { 728 | el.dispatchEvent(keyupEvent); 729 | },10); 730 | }); 731 | } 732 | } 733 | }); 734 | } 735 | }; 736 | 737 | module.exports = VueCardFormat; 738 | -------------------------------------------------------------------------------- /dist/vue-credit-card-validation.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vue-credit-card-validation v1.0.3 3 | * (c) 2022 Michael Wuori 4 | * Released under the MIT License. 5 | */ 6 | (function (global, factory) { 7 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 8 | typeof define === 'function' && define.amd ? define(factory) : 9 | (global.VueCreditCardValidation = factory()); 10 | }(this, (function () { 'use strict'; 11 | 12 | var cards = [ 13 | { 14 | type: 'maestro', 15 | patterns: [ 16 | 5018, 502, 503, 506, 56, 58, 639, 6220, 67, 633 17 | ], 18 | format: /(\d{1,4})/g, 19 | length: [12, 13, 14, 15, 16, 17, 18, 19], 20 | cvcLength: [3], 21 | luhn: true 22 | }, 23 | { 24 | type: 'forbrugsforeningen', 25 | patterns: [600], 26 | format: /(\d{1,4})/g, 27 | length: [16], 28 | cvcLength: [3], 29 | luhn: true 30 | }, 31 | { 32 | type: 'dankort', 33 | patterns: [5019], 34 | format: /(\d{1,4})/g, 35 | length: [16], 36 | cvcLength: [3], 37 | luhn: true 38 | }, 39 | // Credit cards 40 | { 41 | type: 'visa', 42 | patterns: [4], 43 | format: /(\d{1,4})/g, 44 | length: [13, 16], 45 | cvcLength: [3], 46 | luhn: true 47 | }, 48 | { 49 | type: 'mastercard', 50 | patterns: [ 51 | 51, 52, 53, 54, 55, 52 | 22, 23, 24, 25, 26, 27 53 | ], 54 | format: /(\d{1,4})/g, 55 | length: [16], 56 | cvcLength: [3], 57 | luhn: true 58 | }, 59 | { 60 | type: 'amex', 61 | patterns: [34, 37], 62 | format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/, 63 | length: [15, 16], 64 | cvcLength: [3, 4], 65 | luhn: true 66 | }, 67 | { 68 | type: 'dinersclub', 69 | patterns: [30, 36, 38, 39], 70 | format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/, 71 | length: [14], 72 | cvcLength: [3], 73 | luhn: true 74 | }, 75 | { 76 | type: 'discover', 77 | patterns: [60, 64, 65, 622], 78 | format: /(\d{1,4})/g, 79 | length: [16], 80 | cvcLength: [3], 81 | luhn: true 82 | }, 83 | { 84 | type: 'unionpay', 85 | patterns: [62, 88], 86 | format: /(\d{1,4})/g, 87 | length: [16, 17, 18, 19], 88 | cvcLength: [3], 89 | luhn: false 90 | }, 91 | { 92 | type: 'jcb', 93 | patterns: [35], 94 | format: /(\d{1,4})/g, 95 | length: [16], 96 | cvcLength: [3], 97 | luhn: true 98 | } 99 | ]; 100 | 101 | var validation = { 102 | 103 | cardExpiryVal: function (value) { 104 | var ref = Array.from(value.split(/[\s\/]+/, 2)); 105 | var month = ref[0]; 106 | var year = ref[1]; 107 | 108 | // Allow for year shortcut 109 | if (((year != null ? year.length : undefined) === 2) && /^\d+$/.test(year)) { 110 | var prefix = (new Date).getFullYear(); 111 | prefix = prefix.toString().slice(0, 2); 112 | year = prefix + year; 113 | } 114 | 115 | month = parseInt(month, 10); 116 | year = parseInt(year, 10); 117 | 118 | return { month: month, year: year }; 119 | }, 120 | 121 | validateCardNumber: function (num) { 122 | num = (num + '').replace(/\s+|-/g, ''); 123 | if (!/^\d+$/.test(num)) { return false; } 124 | 125 | var card = cardFormatUtils.cardFromNumber(num); 126 | if (!card) { return false; } 127 | 128 | return Array.from(card.length).includes(num.length) && 129 | ((card.luhn === false) || cardFormatUtils.luhnCheck(num)); 130 | }, 131 | 132 | validateCardExpiry: function (month, year) { 133 | 134 | if(!month){ 135 | return false; 136 | } 137 | 138 | if(!year){ 139 | var assign; 140 | ((assign = validation.cardExpiryVal(month), month = assign.month, year = assign.year)); 141 | } 142 | 143 | // Allow passing an object 144 | if ((typeof month === 'object') && 'month' in month) { 145 | var assign$1; 146 | ((assign$1 = month, month = assign$1.month, year = assign$1.year)); 147 | } 148 | 149 | if (!month || !year) { return false; } 150 | 151 | month = month.toString().trim(); 152 | year = year.toString().trim(); 153 | 154 | if (!/^\d+$/.test(month)) { return false; } 155 | if (!/^\d+$/.test(year)) { return false; } 156 | if (!(1 <= month && month <= 12)) { return false; } 157 | 158 | if (year.length === 2) { 159 | if (year < 70) { 160 | year = "20" + year; 161 | } else { 162 | year = "19" + year; 163 | } 164 | } 165 | 166 | console.log(year); 167 | 168 | if (year.length !== 4) { return false; } 169 | 170 | var expiry = new Date(year, month); 171 | var currentTime = new Date; 172 | 173 | // Months start from 0 in JavaScript 174 | expiry.setMonth(expiry.getMonth() - 1); 175 | 176 | // The cc expires at the end of the month, 177 | // so we need to make the expiry the first day 178 | // of the month after 179 | expiry.setMonth(expiry.getMonth() + 1, 1); 180 | 181 | return expiry > currentTime; 182 | }, 183 | 184 | validateCardCVC: function (cvc, type) { 185 | if(!cvc){ 186 | return false; 187 | } 188 | cvc = cvc.toString().trim(); 189 | if (!/^\d+$/.test(cvc)) { return false; } 190 | 191 | var card = cardFormatUtils.cardFromType(type); 192 | if (card != null) { 193 | // Check against a explicit card type 194 | return Array.from(card.cvcLength).includes(cvc.length); 195 | } else { 196 | // Check against all types 197 | return (cvc.length >= 3) && (cvc.length <= 4); 198 | } 199 | }, 200 | 201 | cardType: function (num) { 202 | if (!num) { return null; } 203 | return cardFormatUtils.__guard__(cardFormatUtils.cardFromNumber(num), function (x) { return x.type; }) || null; 204 | }, 205 | 206 | formatCardNumber: function (num) { 207 | 208 | num = num.toString().replace(/\D/g, ''); 209 | var card = cardFormatUtils.cardFromNumber(num); 210 | if (!card) { return num; } 211 | 212 | var upperLength = card.length[card.length.length - 1]; 213 | num = num.slice(0, upperLength); 214 | 215 | if (card.format.global) { 216 | return cardFormatUtils.__guard__(num.match(card.format), function (x) { return x.join(' '); }); 217 | } else { 218 | var groups = card.format.exec(num); 219 | if (groups == null) { return; } 220 | groups.shift(); 221 | // @TODO: Change to native filter() 222 | //groups = grep(groups, n => n); // Filter empty groups 223 | return groups.join(' '); 224 | } 225 | }, 226 | 227 | formatExpiry: function (expiry) { 228 | var parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/); 229 | if (!parts) { return ''; } 230 | 231 | var mon = parts[1] || ''; 232 | var sep = parts[2] || ''; 233 | var year = parts[3] || ''; 234 | 235 | if (year.length > 0) { 236 | sep = ' / '; 237 | 238 | } else if (sep === ' /') { 239 | mon = mon.substring(0, 1); 240 | sep = ''; 241 | 242 | } else if ((mon.length === 2) || (sep.length > 0)) { 243 | sep = ' / '; 244 | 245 | } else if ((mon.length === 1) && !['0', '1'].includes(mon)) { 246 | mon = "0" + mon; 247 | sep = ' / '; 248 | } 249 | 250 | return mon + sep + year; 251 | } 252 | }; 253 | 254 | var cardFormatUtils = { 255 | 256 | cardFromNumber : function (num) { 257 | num = (num + '').replace(/\D/g, ''); 258 | for (var i in cards) { 259 | for (var j in cards[i].patterns) { 260 | var p = cards[i].patterns[j] + ''; 261 | if (num.substr(0, p.length) === p) { return cards[i]; } 262 | } 263 | } 264 | }, 265 | 266 | cardFromType: function (type) { 267 | for (var i in cards) { if (cards[i].type === type) { return cards[i]; } } 268 | }, 269 | 270 | luhnCheck: function (num) { 271 | var odd = true; 272 | var sum = 0; 273 | 274 | var digits = (num + '').split('').reverse(); 275 | 276 | for (var i in digits) { 277 | var digit = parseInt(digits[i], 10); 278 | if (odd = !odd) { digit *= 2; } 279 | if (digit > 9) { digit -= 9; } 280 | sum += digit; 281 | } 282 | 283 | return (sum % 10) === 0; 284 | }, 285 | 286 | hasTextSelected: function (target) { 287 | // If some text is selected 288 | if ((target.selectionStart != null) && 289 | (target.selectionStart !== target.selectionEnd)) { return true; } 290 | 291 | // If some text is selected in IE 292 | if (cardFormatUtils.__guard__(typeof document !== 'undefined' && document !== null ? document.selection : undefined, function (x) { return x.createRange; }) != null) { 293 | if (document.selection.createRange().text) { return true; } 294 | } 295 | 296 | return false; 297 | }, 298 | 299 | // Private 300 | 301 | // Safe Val 302 | 303 | safeVal: function (value, target, e) { 304 | if (e.inputType === 'deleteContentBackward') { 305 | return; 306 | } 307 | var cursor; 308 | try { 309 | cursor = target.selectionStart; 310 | } catch (error) { 311 | cursor = null; 312 | } 313 | var last = target.value; 314 | target.value = value; 315 | value = target.value; 316 | if ((cursor !== null) && document.activeElement == target) { 317 | if (cursor === last.length) { cursor = target.value.length; } 318 | 319 | // This hack looks for scenarios where we are changing an input's value such 320 | // that "X| " is replaced with " |X" (where "|" is the cursor). In those 321 | // scenarios, we want " X|". 322 | // 323 | // For example: 324 | // 1. Input field has value "4444| " 325 | // 2. User types "1" 326 | // 3. Input field has value "44441| " 327 | // 4. Reformatter changes it to "4444 |1" 328 | // 5. By incrementing the cursor, we make it "4444 1|" 329 | // 330 | // This is awful, and ideally doesn't go here, but given the current design 331 | // of the system there does not appear to be a better solution. 332 | // 333 | // Note that we can't just detect when the cursor-1 is " ", because that 334 | // would incorrectly increment the cursor when backspacing, e.g. pressing 335 | // backspace in this scenario: "4444 1|234 5". 336 | if (last !== value) { 337 | var prevPair = last.slice(cursor - 1, +cursor + 1 || undefined); 338 | var currPair = target.value.slice(cursor - 1, +cursor + 1 || undefined); 339 | var digit = value[cursor]; 340 | if (/\d/.test(digit) && 341 | (prevPair === (digit + " ")) && (currPair === (" " + digit))) { cursor = cursor + 1; } 342 | } 343 | 344 | target.selectionStart = cursor; 345 | return target.selectionEnd = cursor; 346 | } 347 | }, 348 | 349 | // Replace Full-Width Chars 350 | 351 | replaceFullWidthChars: function (str) { 352 | if (str == null) { str = ''; } 353 | var fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19'; 354 | var halfWidth = '0123456789'; 355 | 356 | var value = ''; 357 | var chars = str.split(''); 358 | 359 | // Avoid using reserved word `char` 360 | for (var i in chars) { 361 | var idx = fullWidth.indexOf(chars[i]); 362 | if (idx > -1) { 363 | chars[i] = halfWidth[idx]; 364 | } 365 | value += chars[i]; 366 | } 367 | 368 | return value; 369 | }, 370 | 371 | // Format Numeric 372 | 373 | reFormatNumeric: function (e) { 374 | var target = e.currentTarget; 375 | return setTimeout(function () { 376 | var value = target.value; 377 | value = cardFormatUtils.replaceFullWidthChars(value); 378 | value = value.replace(/\D/g, ''); 379 | return cardFormatUtils.safeVal(value, target, e); 380 | }); 381 | }, 382 | 383 | // Format Card Number 384 | 385 | reFormatCardNumber: function (e) { 386 | var target = e.currentTarget; 387 | return setTimeout(function () { 388 | var value = target.value; 389 | value = cardFormatUtils.replaceFullWidthChars(value); 390 | value = validation.formatCardNumber(value); 391 | return cardFormatUtils.safeVal(value, target, e); 392 | }); 393 | }, 394 | 395 | formatCardNumber: function (e) { 396 | // Only format if input is a number 397 | var re; 398 | var digit = String.fromCharCode(e.which); 399 | if (!/^\d+$/.test(digit)) { return; } 400 | 401 | var target = e.currentTarget; 402 | var value = target.value; 403 | var card = cardFormatUtils.cardFromNumber(value + digit); 404 | var length = (value.replace(/\D/g, '') + digit); 405 | 406 | var upperLength = 16; 407 | if (card) { upperLength = card.length[card.length.length - 1]; } 408 | if (length >= upperLength) { return; } 409 | 410 | // Return if focus isn't at the end of the text 411 | if ((target.selectionStart != null) && 412 | (target.selectionStart !== value.length)) { return; } 413 | 414 | if (card && (card.type === 'amex')) { 415 | // AMEX cards are formatted differently 416 | re = /^(\d{4}|\d{4}\s\d{6})$/; 417 | } else { 418 | re = /(?:^|\s)(\d{4})$/; 419 | } 420 | 421 | // If '4242' + 4 422 | if (re.test(value + digit)) { 423 | e.preventDefault(); 424 | return setTimeout(function () { return target.value = value + ' ' + digit; }); 425 | 426 | // If '424' + 2 427 | } else if (re.test(value + digit)) { 428 | e.preventDefault(); 429 | return setTimeout(function () { return target.value = value + digit + ' '; }); 430 | } 431 | 432 | }, 433 | 434 | formatBackCardNumber: function (e) { 435 | var target = e.currentTarget; 436 | var value = target.value; 437 | 438 | // Return unless backspacing 439 | if (e.which !== 8) { return; } 440 | 441 | // Return if focus isn't at the end of the text 442 | if ((target.selectionStart != null) && 443 | (target.selectionStart !== value.length)) { return; } 444 | 445 | // Remove the digit + trailing space 446 | if (/\d\s$/.test(value)) { 447 | e.preventDefault(); 448 | return setTimeout(function () { return target.value = value.replace(/\d\s$/, ''); }); 449 | // Remove digit if ends in space + digit 450 | } else if (/\s\d?$/.test(value)) { 451 | e.preventDefault(); 452 | return setTimeout(function () { return target.value = value.replace(/\d$/, ''); }); 453 | } 454 | }, 455 | 456 | // Format Expiry 457 | 458 | reFormatExpiry: function (e) { 459 | var target = e.currentTarget; 460 | return setTimeout(function () { 461 | var value = target.value; 462 | value = cardFormatUtils.replaceFullWidthChars(value); 463 | value = validation.formatExpiry(value); 464 | return cardFormatUtils.safeVal(value, target, e); 465 | }); 466 | }, 467 | 468 | formatExpiry: function (e) { 469 | // Only format if input is a number 470 | var digit = String.fromCharCode(e.which); 471 | if (!/^\d+$/.test(digit)) { return; } 472 | 473 | var target = e.currentTarget; 474 | var val = target.value + digit; 475 | 476 | if (/^\d$/.test(val) && !['0', '1'].includes(val)) { 477 | e.preventDefault(); 478 | return setTimeout(function () { return target.value = (("0" + val + " / ")); }); 479 | 480 | } else if (/^\d\d$/.test(val)) { 481 | e.preventDefault(); 482 | return setTimeout(function () { 483 | // Split for months where we have the second digit > 2 (past 12) and turn 484 | // that into (m1)(m2) => 0(m1) / (m2) 485 | var m1 = parseInt(val[0], 10); 486 | var m2 = parseInt(val[1], 10); 487 | if ((m2 > 2) && (m1 !== 0)) { 488 | return target.value = (("0" + m1 + " / " + m2)); 489 | } else { 490 | return target.value = ((val + " / ")); 491 | } 492 | }); 493 | } 494 | }, 495 | 496 | formatForwardExpiry: function (e) { 497 | var digit = String.fromCharCode(e.which); 498 | if (!/^\d+$/.test(digit)) { return; } 499 | 500 | var target = e.currentTarget; 501 | var val = target.value; 502 | 503 | if (/^\d\d$/.test(val)) { 504 | return target.value = ((val + " / ")); 505 | } 506 | }, 507 | 508 | formatForwardSlashAndSpace: function (e) { 509 | var which = String.fromCharCode(e.which); 510 | if ((which !== '/') && (which !== ' ')) { return; } 511 | 512 | var target = e.currentTarget; 513 | var val = target.value; 514 | 515 | if (/^\d$/.test(val) && (val !== '0')) { 516 | return target.value = (("0" + val + " / ")); 517 | } 518 | }, 519 | 520 | formatBackExpiry: function (e) { 521 | var target = e.currentTarget; 522 | var value = target.value; 523 | 524 | // Return unless backspacing 525 | if (e.which !== 8) { return; } 526 | 527 | // Return if focus isn't at the end of the text 528 | if ((target.selectionStart != null) && 529 | (target.selectionStart !== value.length)) { return; } 530 | 531 | // Remove the trailing space + last digit 532 | if (/\d\s\/\s$/.test(value)) { 533 | e.preventDefault(); 534 | return setTimeout(function () { return target.value = value.replace(/\d\s\/\s$/, ''); }); 535 | } 536 | }, 537 | 538 | // Adds maxlength to Expiry field 539 | handleExpiryAttributes: function(e){ 540 | e.setAttribute('maxlength', 9); 541 | }, 542 | 543 | // Format CVC 544 | reFormatCVC: function (e) { 545 | var target = e.currentTarget; 546 | return setTimeout(function () { 547 | var value = target.value; 548 | value = cardFormatUtils.replaceFullWidthChars(value); 549 | value = value.replace(/\D/g, '').slice(0, 4); 550 | return cardFormatUtils.safeVal(value, target, e); 551 | }); 552 | }, 553 | 554 | // Restrictions 555 | restrictNumeric: function (e) { 556 | 557 | // Key event is for a browser shortcut 558 | if (e.metaKey || e.ctrlKey) { return true; } 559 | 560 | // If keycode is a space 561 | if (e.which === 32) { return false; } 562 | 563 | // If keycode is a special char (WebKit) 564 | if (e.which === 0) { return true; } 565 | 566 | // If char is a special char (Firefox) 567 | if (e.which < 33) { return true; } 568 | 569 | var input = String.fromCharCode(e.which); 570 | 571 | // Char is a number or a space 572 | return (!!/[\d\s]/.test(input)) ? true : e.preventDefault(); 573 | }, 574 | 575 | restrictCardNumber: function (e) { 576 | var target = e.currentTarget; 577 | var digit = String.fromCharCode(e.which); 578 | if (!/^\d+$/.test(digit)) { return; } 579 | if (cardFormatUtils.hasTextSelected(target)) { return; } 580 | // Restrict number of digits 581 | var value = (target.value + digit).replace(/\D/g, ''); 582 | var card = cardFormatUtils.cardFromNumber(value); 583 | 584 | if (card) { 585 | return value.length <= card.length[card.length.length - 1]; 586 | } else { 587 | // All other cards are 16 digits long 588 | return value.length <= 16; 589 | } 590 | }, 591 | 592 | restrictExpiry: function (e) { 593 | var target = e.currentTarget; 594 | var digit = String.fromCharCode(e.which); 595 | if (!/^\d+$/.test(digit)) { return; } 596 | 597 | if (cardFormatUtils.hasTextSelected(target)) { return; } 598 | 599 | var value = target.value + digit; 600 | value = value.replace(/\D/g, ''); 601 | 602 | if (value.length > 6) { return false; } 603 | }, 604 | 605 | restrictCVC: function (e) { 606 | var target = e.currentTarget; 607 | var digit = String.fromCharCode(e.which); 608 | if (!/^\d+$/.test(digit)) { return; } 609 | 610 | if (cardFormatUtils.hasTextSelected(target)) { return; } 611 | 612 | var val = target.value + digit; 613 | return val.length <= 4; 614 | }, 615 | 616 | setCardType: function (e) { 617 | 618 | var target = e.currentTarget; 619 | var val = target.value; 620 | var cardType = validation.cardType(val) || 'unknown'; 621 | 622 | if (target.className.indexOf(cardType) === -1) { 623 | 624 | var allTypes = []; 625 | for(var i in cards){ 626 | allTypes.push(cards[i].type); 627 | } 628 | 629 | target.classList.remove('unknown'); 630 | target.classList.remove('identified'); 631 | (ref = target.classList).remove.apply(ref, allTypes); 632 | target.classList.add(cardType); 633 | target.dataset.cardBrand = cardType; 634 | 635 | if(cardType !== 'unknown'){ 636 | target.classList.add('identified'); 637 | } 638 | 639 | } 640 | var ref; 641 | }, 642 | 643 | __guard__: function (value, transform) { 644 | return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; 645 | } 646 | 647 | }; 648 | 649 | var format = { 650 | 651 | validateCardNumber: validation.validateCardNumber, 652 | validateCardCVC: validation.validateCardCVC, 653 | validateCardExpiry: validation.validateCardExpiry, 654 | 655 | setCardType: function(el) { 656 | cardFormatUtils.setCardType(el); 657 | setTimeout(function(){ 658 | el.currentTarget.dispatchEvent(new Event('keyup')); 659 | el.currentTarget.dispatchEvent(new Event('change')); 660 | }, 100); 661 | }, 662 | 663 | formatCardCVC: function (el) { 664 | el.addEventListener('keypress', cardFormatUtils.restrictNumeric); 665 | el.addEventListener('keypress', cardFormatUtils.restrictCVC); 666 | el.addEventListener('paste', cardFormatUtils.reFormatCVC); 667 | el.addEventListener('change', cardFormatUtils.reFormatCVC); 668 | el.addEventListener('input', cardFormatUtils.reFormatCVC); 669 | return this; 670 | }, 671 | 672 | formatCardExpiry: function (el) { 673 | cardFormatUtils.handleExpiryAttributes(el); 674 | el.addEventListener('keypress', cardFormatUtils.restrictNumeric); 675 | el.addEventListener('keypress', cardFormatUtils.formatExpiry); 676 | el.addEventListener('keypress', cardFormatUtils.formatForwardSlashAndSpace); 677 | el.addEventListener('keypress', cardFormatUtils.formatForwardExpiry); 678 | el.addEventListener('keydown', cardFormatUtils.formatBackExpiry); 679 | el.addEventListener('change', cardFormatUtils.reFormatExpiry); 680 | el.addEventListener('input', cardFormatUtils.reFormatExpiry); 681 | el.addEventListener('blur', cardFormatUtils.reFormatExpiry); 682 | return this; 683 | }, 684 | 685 | formatCardNumber: function (el) { 686 | el.maxLength = 19; 687 | el.addEventListener('keypress', cardFormatUtils.restrictNumeric); 688 | el.addEventListener('keypress', cardFormatUtils.restrictCardNumber); 689 | el.addEventListener('keypress', cardFormatUtils.formatCardNumber); 690 | el.addEventListener('keydown', cardFormatUtils.formatBackCardNumber); 691 | el.addEventListener('keyup', cardFormatUtils.setCardType); 692 | el.addEventListener('paste', cardFormatUtils.reFormatCardNumber); 693 | el.addEventListener('change', cardFormatUtils.reFormatCardNumber); 694 | el.addEventListener('input', cardFormatUtils.reFormatCardNumber); 695 | el.addEventListener('input', cardFormatUtils.setCardType); 696 | return this; 697 | }, 698 | 699 | restrictNumeric: function (el) { 700 | el.addEventListener('keypress', cardFormatUtils.restrictNumeric); 701 | el.addEventListener('paste', cardFormatUtils.restrictNumeric); 702 | el.addEventListener('change', cardFormatUtils.restrictNumeric); 703 | el.addEventListener('input', cardFormatUtils.restrictNumeric); 704 | return this; 705 | } 706 | }; 707 | 708 | var VueCardFormat = { 709 | install: function install(vue, opts) { 710 | // provide plugin to Vue 711 | vue.config.globalProperties.$cardFormat = format; 712 | // provide directive 713 | vue.directive('cardformat', { 714 | beforeMount: function beforeMount(el, binding, vnode) { 715 | // see if el is an input 716 | if (el.nodeName.toLowerCase() !== 'input'){ 717 | el = el.querySelector('input'); 718 | } 719 | // call format function from prop 720 | var method = Object.keys(format).find(function (key) { return key.toLowerCase() === binding.arg.toLowerCase(); }); 721 | var keyupEvent = new Event('keyup'); 722 | format[method](el, vnode); 723 | // update cardBrand value if available 724 | if (method == 'formatCardNumber' && typeof binding.instance.cardBrand !== 'undefined'){ 725 | el.addEventListener('keyup', function () { 726 | if (el.dataset.cardBrand) { 727 | binding.instance.cardBrand = el.dataset.cardBrand; 728 | } 729 | }); 730 | el.addEventListener('paste', function () { 731 | setTimeout(function () { 732 | el.dispatchEvent(keyupEvent); 733 | },10); 734 | }); 735 | } 736 | } 737 | }); 738 | } 739 | }; 740 | 741 | return VueCardFormat; 742 | 743 | }))); 744 | --------------------------------------------------------------------------------