A Comprehensive Guide to Validating and Formatting Credit Cards

Kelvin Zhang
Aug 31, 2018 · 9 min read

Credit card forms are one of those elements that every online business will have to implement at one point or another, and they can often be a pain point for both developers and users. That being said, it’s crucial that these forms are designed to be user-friendly and intuitive as they are the entry point for paying users: it would be a shame if a company lost a potential customer to a badly-designed payment form, even if they did everything else right.

The goal of this post is to explain how to implement your own super-awesome credit card form, complete with bug-free validation and formatting. If you’re only here for the code, feel free to scroll to the very bottom to view the final implementation.

Table of Contents


Types of Credit Cards

A list of common credit cards and their properties

A couple of things to note here:

  • As part of Mastercard’s 2-series expansion, their cards can now begin with 2
  • American Express has an unusual CVV that is 4 digits long
  • The cards that you support should depend on what your selected payment gateway supports. It’s bad practice to allow unsupported cards to pass through your client-side validation.
  • Maestro is a complete pain to deal with and is unsupported by most payment gateways. As such, Maestro will not be covered in the remainder of this post.

For a list of common payment gateways and the cards they support, see this list by Aria Systems. For a list of test cards, see this Github repo. For more cards and their RegEx patterns, view this list on Github.


Validating Cards

Why?

  • The user gets immediate feedback on an input error; if an invalid card is entered, they don’t need to click on the submit button, wait for the server to return an error, and then fill out the form again.
  • It lessens your server load and prevents invalid requests from counting towards your API rate limit.

The Luhn Algorithm

Credit card numbers may look random, but there is actually a hidden meaning behind each group of numbers.

In the above diagram:

  1. Major Industry Identifier (MII) — identifies the industry of the card. See here for a list of industries and their corresponding digits.
  2. Issuer Identification Number (IIN) — identifies the issuer of the card. American Express starts with 34 or 37, Mastercard starts with 2221–2720 or 51–55, Visa starts with 4. See here for a list of all IIN ranges. This is especially useful for future updates if card issuers ever decide to expand their IIN ranges.
  3. Account Number — identifies the customer’s account
  4. Checksum — makes sure that the account number is valid

The Luhn Algorithm determines the validity of a card using the account number and checksum (labels 3 and 4). It works almost like magic:

  1. From the rightmost digit of your card number, double every other digit.
  2. If the doubled digit is larger than 9 (ex. 8 * 2 = 16), subtract 9 from the product (16–9 = 7).
  3. Sum the digits.
  4. If there is no remainder after dividing by 10 (sum % 10 == 0), the card is valid.

Using the card from above, here is the Luhn Algorithm in action:

Summing up the last row gives us a value of 90, which is a multiple of 10. This card is valid!

Here’s a Javascript implementation of the Luhn Algorithm:

function checkLuhn(value) {
// remove all non digit characters
var value = value.replace(/\D/g, '');
var sum = 0;
var shouldDouble = false;
// loop through values starting at the rightmost side
for (var i = value.length - 1; i >= 0; i--) {
var digit = parseInt(value.charAt(i));

if (shouldDouble) {
if ((digit *= 2) > 9) digit -= 9;
}

sum += digit;
shouldDouble = !shouldDouble;
}
return (sum % 10) == 0;
}

Demo

You can view implementations of the Luhn Algorithm in other languages such as Java, Swift, PHP, and Python here.

Checking for Supported Cards

With reference to the list of cards above and their specifications, we can create a validator based on the RegEx for each specific card.

The best way to keep track of different cards and their patterns is to store them in an object literal:

var acceptedCreditCards = {
visa: /^4[0-9]{12}(?:[0-9]{3})?$/,
mastercard: /^5[1-5][0-9]{14}$|^2(?:2(?:2[1-9]|[3-9][0-9])|[3-6][0-9][0-9]|7(?:[01][0-9]|20))[0-9]{12}$/,
amex: /^3[47][0-9]{13}$/,
discover: /^65[4-9][0-9]{13}|64[4-9][0-9]{13}|6011[0-9]{12}|(622(?:12[6-9]|1[3-9][0-9]|[2-8][0-9][0-9]|9[01][0-9]|92[0-5])[0-9]{10})$/,
diners_club: /^3(?:0[0-5]|[68][0-9])[0-9]{11}$/,
jcb: /^(?:2131|1800|35[0-9]{3})[0-9]{11}$/
};

We can then create a function that tests the inputted value against all RegEx patterns to determine the card’s validity:

function checkSupported(value) {
// remove all non digit characters
var value = value.replace(/\D/g, '');
var accepted = false;

// loop through the keys (visa, mastercard, amex, etc.)
Object.keys(acceptedCreditCards).forEach(function(key) {
var regex = acceptedCreditCards[key];
if (regex.test(value)) {
accepted = true;
}
});

return accepted;
}

Demo

Putting it Together

Finally, we can combine the Luhn algorithm with our supported credit cards checker to complete our magical validation formula.

function validateCard(value) {
// remove all non digit characters
var value = value.replace(/\D/g, '');
var sum = 0;
var shouldDouble = false;
// loop through values starting at the rightmost side
for (var i = value.length - 1; i >= 0; i--) {
var digit = parseInt(value.charAt(i));

if (shouldDouble) {
if ((digit *= 2) > 9) digit -= 9;
}

sum += digit;
shouldDouble = !shouldDouble;
}

var valid = (sum % 10) == 0;
var accepted = false;

// loop through the keys (visa, mastercard, amex, etc.)
Object.keys(acceptedCreditCards).forEach(function(key) {
var regex = acceptedCreditCards[key];
if (regex.test(value)) {
accepted = true;
}
});

return valid && accepted;
}

Demo


Validating the CVV

Why?

  • For the exact same reasons as validating credit card numbers: to reduce the number of invalid requests being made to the server.

The card verification value (CVV) is a set of 3–4 digit numbers on the back of your card and is used for security reasons. Most CVVs are 3 digits, with the exception of Maestro, which may not even require a CVV, and American Express, which has a CVV of 4 digits. Since we’re not supporting Maestro, American Express is the only exception we’ll have to make.

A CVV doesn’t have anything like a Luhn algorithm to check its validity, so all we have to do is check its length:

function validateCVV(creditCard, cvv) {
// remove all non digit characters
var creditCard = creditCard.replace(/\D/g, '');
var cvv = cvv.replace(/\D/g, '');
// american express and cvv is 4 digits
if ((acceptedCreditCards.amex).test(creditCard)) {
if((/^\d{4}$/).test(cvv))
return true;
} else if ((/^\d{3}$/).test(cvv)) { // other card & cvv is 3 digits
return true;
}
return false;
}

Let’s also set a maxlength for it:

$('#cvv').attr('maxlength', 4);

We can then integrate this with our credit card validation.

Demo

Toggling the Submit Button

When the credit card or CVV is invalid, we should disable the submit button because we don’t want invalid form data to be sent to the server. This is as easy as changing the #status element to a submit button and then toggling the disabled prop.

Demo


Formatting Cards

Why?

  • The user can see at a glance whether they missed or added an extra character
  • It’s easier for the user to go back and change a digit in the case of typos

There are also a couple of UX goals we want to accomplish when adding auto-formatting:

  1. We don’t want to disallow the user from typing spaces as they enter their card number
  2. The user should be able to insert and remove digits before and after a formatted space
  3. The cursor position should be retained when inserting and removing digits
  4. When formatting is changed (ex. American Express → Visa), digits should be re-formatted to match the new layout

With those goals in mind, here are a couple of approaches to formatting cards:

Input Masking Libraries

Advantages:

  • many libraries to choose from
  • easy to implement
  • wide variety of pre-built masks

Disadvantages:

  • some are bulky and slow to load
  • many have bugs that are hard to fix without modifying the source
  • all give you less control over what’s happening

Here are some of the input masking libraries I’ve tested:

Plain Javascript: https://github.com/RobinHerbots/Inputmask (180KB)
React: https://github.com/estelle/input-masking (5KB)
Angular, Ember, Vue: https://github.com/text-mask/text-mask (4KB)

Regardless of the library used, the logic behind each implementation should be similar:

$("#cc").on("input propertychange paste", function() {
var value = $("#cc").val().replace(/\D/g, '');
var mask;
if ((/^3[47]\d{0,13}$/).test(value)) { // American Express
// set mask to 4-6-5
} else if ((/^3(?:0[0-5]|[68]\d)\d{0,11}$/).test(value)) { // Diner's Club
// set mask to 4-6-4
} else if ((/^\d{0,16}$/).test(value)) { // Other Credit Cards
// set mask to 4-4-4-4
}

// apply your input mask to #cc
});

Here’s an implementation of RobinHerbots’s Inputmask, which I believe to be the best library from the list above. Although it is significantly larger in size and comes with an array of unnecessary features, it allows user input of spaces, inserting/removing digits after spaces, and re-formatting of cards.

However, the cursor position isn’t retained if the card is re-formatted. If you start out by entering an American Express number (ex. 3782 822463 10005), and then delete the 3 in the beginning, the card is re-formatted correctly but the cursor skips to the end.

Although this isn’t that big of an issue, I wasn’t happy with it. It seemed that whatever library I used would miss out on at least one of the 4 goals. In the end, I got fed up and decided to implement my own input mask.

Custom Input Masking

I wanted my custom input mask to achieve all of the 4 goals while also retaining some quality of life features of input masking libraries, such as limiting the length. At its core, an input mask updates the current input value with the correctly formatted value.

To accomplish this, I created a function that takes in a card number and outputs the correctly formatted number. In this function, I also limit the length of the input depending on the card type:

function formatCardNumber(value) {
// remove all non digit characters
var value = value.replace(/\D/g, '');
var formattedValue;
var maxLength;
// american express, 15 digits
if ((/^3[47]\d{0,13}$/).test(value)) {
formattedValue = value.replace(/(\d{4})/, '$1 ').replace(/(\d{4}) (\d{6})/, '$1 $2 ');
maxLength = 17;
} else if((/^3(?:0[0-5]|[68]\d)\d{0,11}$/).test(value)) { // diner's club, 14 digits
formattedValue = value.replace(/(\d{4})/, '$1 ').replace(/(\d{4}) (\d{6})/, '$1 $2 ');
maxLength = 16;
} else if ((/^\d{0,16}$/).test(value)) { // regular cc number, 16 digits
formattedValue = value.replace(/(\d{4})/, '$1 ').replace(/(\d{4}) (\d{4})/, '$1 $2 ').replace(/(\d{4}) (\d{4}) (\d{4})/, '$1 $2 $3 ');
maxLength = 19;
}

$('#cc').attr('maxlength', maxLength);
return formattedValue;
}

The core functionality is achieved by a chain of .replace methods. This allows for the card to be formatted as it is being typed. As such, we’re also not making use of the acceptedCreditCards object that we defined earlier on. The RegEx is modified to match the IIN ranges of each issuer. For example, we can change the mask to 4–6–5 as soon as 34 or 37 is entered (American Express). Furthermore, for the cards that we support, only American Express (15 digits) and Diner’s Club (14 digits) require special formatting.

We can then update our input to reflect the formatted value:

$('#cc').on('input', function() {
var value = $('#cc').val();
var formattedValue = formatCardNumber(value);
$('#cc').val(formattedValue);
});

Demo

For 25 lines of code, this isn’t too bad. It allows user-input of spaces and re-formats credit card numbers. However, deleting any digit or inserting a digit before a space will move the cursor to the end. You also can’t delete spaces.

All of these bugs happen because updating the value of an input will move the cursor to the end. We can fix this by storing the cursor position and then updating it. There are also two blocks here that adjust the cursor position to allow for the removal of spaces and the insertion of digits before a space.

$('#cc').on('input', function() {
var node = $('#cc')[0]; // vanilla javascript element
var cursor = node.selectionStart; // store cursor position
var lastValue = $('#cc').val(); // get value before formatting

var formattedValue = formatCardNumber(lastValue);
$('#cc').val(formattedValue); // set value to formatted

// keep the cursor at the end on addition of spaces
if(cursor === lastValue.length) {
cursor = formattedValue.length;
// decrement cursor when backspacing
// i.e. "4444 |" => backspace => "4444|"
if($('#cc').attr('data-lastvalue') && $('#cc').attr('data-lastvalue').charAt(cursor - 1) == " ") {
cursor--;
}
}

if (lastValue !== formattedValue) {
// increment cursor when inserting character before a space
// i.e. "1234| 6" => "5" typed => "1234 5|6"
if(lastValue.charAt(cursor) == " " && formattedValue.charAt(cursor - 1) == " ") {
cursor++;
}
}

// set cursor position
node.selectionStart = cursor;
node.selectionEnd = cursor;
// store last value
$('#cc').attr('data-lastvalue', formattedValue);
});

Demo

Perfect! Now users can type spaces as they’re entering their card number, insert/remove digits before and after a formatted space, retain the cursor position when a digit is inserted or removed, and, when formatting is changed, re-format the card while preserving the cursor position.


Wrapping It Up

Now that we have the validation and formatting of credit cards complete, let’s combine them into the ultimate credit card form.

Final result

Hootsuite Engineering

Hootsuite's Engineering Blog

Kelvin Zhang

Written by

Software Engineering Intern @ Hootsuite 🦉, UWaterloo SE

Hootsuite Engineering

Hootsuite's Engineering Blog

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade