Angular Card Masking and Validation

Visa, MasterCard, Maestro, Diners Club, American Express, Discover, JCB — so many cards a user can pay with! No matter whether you’re building a fintech app or an online store, you would probably have to validate the card number.

Note: We built a credit card generator based on ideas from this article. See it in action!

What are we building?

You can have a look at the resulting code here. The app identifies the card brand, then verifies the card number. It also applies the proper number spacing for each card brand.

Demo card numbers to try:

  • Valid VISA: 4888524251678924
  • Invalid VISA: 4888524251678923
  • Diners club: 5531452809590417
  • Completely invalid card: 1234 (not allowed to type a number past 1)

Let’s start with the basics…

The bank card number contains a lot of information for the issuer and the cardholder which we won’t look at in depth in this post. By looking at the first digit, we can tell the issuer’s brand and industry (every digit from 0 to 9 is reserved for a specific industry):

0 — ISO/TC 68 and other industry assignments

1 — Airlines

2 — Airlines, financial and other future industry assignments

3 — Travel and entertainment

4 — Banking and financial

5 — Banking and financial

6 — Merchandising and banking/financial

7 — Petroleum and other future industry assignments

8 — Healthcare, telecommunications and other future industry assignments

9 — For assignment by national standards bodies

Card numbers can also be of different length and different spacing. It is also possible that one brand has a range of valid card number length — for example, VISA cards can have 13 to 19 digits.

This being said, the only unique identifier is the first digit(s) of the card. Below, you can see the corresponding digit for the most commonly used card brands:

Dynamic card masking with minimal libraries

To improve user experience, it’s a great thing to check the validity of the card number dynamically, every time the user types in a digit. To do it quickly, you can use a masking library, such as angular2-text-mask. It would require a masking function, returning a mask on every input value from the user. And this is really all you need!

Luhn’s algorithm

To bring your card number validation to the next level, you can include Luhn’s Algorithm. It’s widely used for validation of a variety of identification numbers including credit card numbers. It’s a checksum formula, containing a few simple steps. Let’s take a look at an example:

Original number: 4916507764842828

  1. Drop the last digit 4 9 1 6 5 0 7 7 6 4 8 4 2 8 2
  2. Reverse the number 2 8 2 4 8 4 6 7 7 0 5 6 1 9 4
  3. Multiply the digits in odd positions (1,3,5…) by 2 4 8 4 4 16 4 12 7 14 0 10 6 2 9 8
  4. Subtract 9 from all numbers bigger than 9 4 8 4 4 7 4 3 7 5 0 1 6 2 9 8
  5. Add all numbers together 4 + 8 + 4 + 4 + 7 + 4 + 3 + 7 + 5 + 0 + 1 + 6 + 2 + 9 + 8 = 72
  6. Add the obtained sum in step 5 and the dropped digit from step 1 72 + 8 = 80
  7. Find the modulo 10 of the obtained sum from step 6 80 modulo 10 = 0
  8. If the modulo is 0, the card number is valid

What about exceptions?

As per the saying “Every rule has an exception” this is no different. Luhn’s Algorithm is applicable to around 95% of the cases. However, to make it to 100%, you need to exclude the following from the validation list:

  • Diners Club enRoute
  • China UnionPay

3, 2, 1, Action!

It’s really easy to set it up! We have even provided card masks and profiles, ready for use, as well as a custom Luhn validator. To start, you just need to set up a simple form with a cardNumber control, as follows:

this.cardNumberGroup = new FormGroup({
  cardNumber: new FormControl('', [
          Validators.required,
          Validators.minLength(12),
          luhnValidator()
        ])
});

Coming to validation, we need a few things here. As we already know, Luhn’s algorithm is used for validation of different types of ID numbers with different length. This is why, a custom Luhn validator may not be enough for an accurate validation. To avoid getting a valid control for too short numbers, we can add a simple length validator.

Back to Luhn, the custom validator function looks like this:

export function luhnValidator(): ValidatorFn {
  return (control: AbstractControl) => {
    const isValid = luhnCheck(control.value);
    return isValid ? null:  {'luhnCheck': isValid};
  };
}

In case you’re not familiar with custom Validators, you can quickly check out Angular’s official documentation.

The Luhn check, itself, follows the steps, explained above.

export const luhnCheck = (cardNumber: string): boolean => {
  if(!cardNumber.length){
    return;
  }

  // Remove all whitespaces from card number.
  cardNumber = cardNumber.replace(/\s/g,'');

  // 1. Remove last digit;
  const lastDigit = Number(cardNumber[cardNumber.length - 1]);

  // 2. Reverse card number
  const reverseCardNumber = cardNumber
    .slice(0,cardNumber.length - 1)
    .split('')
    .reverse()
    .map(x => Number(x));
  
  let sum = 0;

  // 3. + 4. Multiply by 2 every digit on odd position.
  // Subtract 9 if digit > 9
  for(let i = 0; i <= reverseCardNumber.length -1; i += 2){
    reverseCardNumber[i] = reverseCardNumber[i]*2;
    if(reverseCardNumber[i] > 9){
      reverseCardNumber[i] = reverseCardNumber[i] - 9;
    }
  }

  // 5. Make the sum of obtained values from step 4.
  sum = reverseCardNumber
    .reduce((acc, currValue) => (acc + currValue), 0);

  // 6. Calculate modulo 10 of the sum from step 5 and the last digit. 
  // If it's 0, you have a valid card number :)
  return ((sum + lastDigit) % 10 === 0);
};

With this, our validation work is done.

Next, let’s look at how to create an input mask. To do this easier and faster, we have used angular2-text-mask library. Let’s install it quickly with npm i angular2-text-mask —-save and include it in our AppModule:

import { TextMaskModule } from 'angular2-text-mask';

@NgModule({
  imports:      [ TextMaskModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

Before we hook it up to our input field, let’s create a card mask function which will dynamically give us the right input mask.

cardMaskFunction(rawValue: string): Array<RegExp> {
  const card = getValidationConfigFromCardNo(rawValue);
  if (card) {
    return card.mask;
  }
  return [/\d/];
}

To make the code cleaner, we have moved all card logic into a card.helper.ts file, where you can find different card profiles and card number masks, as well as the getValidationConfigFromCardNo() :

export const getValidationConfigFromCardNo = (
  rawValue: string
): CardValidation =>
  cards.find(card => {
    const patterns = card.patterns.map(
      pattern => new RegExp(`^${pattern}`, 'g')
    );
    const matchResult = patterns
      .map(pattern => rawValue.match(pattern))
      .filter(result => result);

    return !!matchResult.length;
  }) || null;

Now that we have all of this ready, let’s add the mask to our template:

<input [formControl]="getCardNumberControl()"
       [textMask]="{mask: cardMaskFunction, guide: false, showMask: true}"
       placeholder="Card Number">

Now you have a fully functional, nicely formatted card number input!

Tags

Angular
Validation
menu_icon
Gabriel Stellini

18th December 2019