Analyzing React Credit Card Validation
February 21st, 2020
Motivation
I’ve always known about credit card checksums and thought it would be an interesting activity to work on. I didn’t want to simply calculate it by running Python from the command line, though. I wanted there to be an interface and I figured it would be fun to mess around in React, anyway.
Plus, there’s just something nice about seeing the logos light up. I’ve seen that concept elsewhere when entering details for online purchases, so I wanted to make it happen in this project.
Demo
View the demo on GitHub or on in projects.
Source code
The source code for this project is on GitHub.
Structure
App
App
returns a header
, a div
containing <CreditCardForm />
, and a footer
.
CreditCardForm
This is where everything of interest happens, of course.
Componentization methodology
Since button
is simple and only occurs once, it was left as is. input
is a controlled component, but was not broken out into its own component. It certainly could have been, though, and may be a likely candidate in the future.
However, since I need to output multiple logos that will have varying state that may be unique from each other based on user input, they were broken out into their own component. A clearer line of reasoning about each Logo
seemed to be best.
Execution flow
When input is received, a number of things happen:
handleChange
setscardNumber
to the input’s valuecomponentDidUpdate
is triggered after the state is changed and the component is re-rendered- multiple conditions are checked inside
componentDidUpdate
comparing previous state to current state to determine which operation(s) to apply - those operations then call various functions, such as
determineType
,verifyNumber
, andgetValidMessage
- functions then may call additional functions based on conditions, such as
purgeInactive
Let’s explore what those functions do and why they’re necessary.
determineType
We loop through prefixes
, which is a Map
containing the supported issuers (Visa, Mastercard, Discover, and American Express) as keys and each issuer identification number (IIN), also known as prefixes, corresponding to that issuer.
Visa and American Express have few prefixes while Mastercard and Discover have hundreds.
If the start of the input matches any of the prefixes in the list, we set the state of the component’s type
to the matching issuer, call purgeInactive
, and return from the function.
verifyNumber
We utilize the Luhn algorithm used by most credit card issuers to determine the potential validity of a given credit card number. The reason we say that it is only the potential validity is because the algorithm with not detect certain errors, such as the transposition of 09 to 90 (per Wikipedia).
More robust algorithms for this purpose do exist: see the Verhoeff and Damm algorithms.
We begin by setting sum = 0
, making a copy of the component’s cardNumber
, setting the checksum digit to the last digit of the copy, and setting parity
to the length of the input modulo 2.
The algorithm is as follows:
- Double every other digit, starting from the second-to-last digit, and if
- the result is greater than 9: add the individual digits or subtract 9 from the result
- otherwise, do nothing
- Sum all the digits.
- If
sum mod 10 === 0
, the number is potentially valid.
For the doubling operation, we check that i % 2 === parity
. That is, we make sure our loop variable i
is at an index that corresponds to starting from the second-to-last digit and doubling every other digit. Whether we start from the second-to-last or the first digit programmatically, the result is the same. As such, we loop through it forward as it’s easier to reason about.
If the sum
, making sure to include its checkDigit
divides evenly into 10, we return true
. Otherwise, we return false
.
getValidMessage
We return a message indicating validity (positive or negative) based on the component’s value for valid
. This is meant to fire only if cardNumber.length === maxLength
.
purgeInactive
To ensure that no other logos remain active when input is changed, we call purgeInactive
. This is only called when another logo becomes active by having the input’s start match a prefix. It solved a bug where once another logo have become active, it would not becoming inactive if we selected the entire input and immediately replaced it with a number that matched another issuer’s prefix.
Immediately replacing input in this manner led to several issues, one of which remains and is detailed below.
reset
A reset button
allows us to quickly clear the input and validity message, returning us to a clean state.
Lessons Learned
Pitfalls of forEach
We can’t use return
, break
, or continue
inside forEach
. Since it uses a callback function on each iteration, returning from any given iteration doesn’t affect the other calls to that callback function on future iterations. Similarly, we can’t break
or continue
as we’re not actually in a loop — rather, we’re inside the callback function itself.
Changing a class based on a dynamic value
By default, a Logo
is grayed out, or inactive, using opacity: 0.5
. This makes sense as a card number of length 0
should never correspond to any issuer. If we enter a number whose start matches a known prefix, the corresponding issuer’s Logo
is no longer inactive, using opacity: 1
. If the input no longer corresponds to that issuer, the Logo
reverts to its inactive state. This occurs in real time.
To do this, we need to change the opacity of the corresponding Logo
only. We used activeVisa
, activeMastercard
, activeDiscover
, and activeAmex
to store the active state of each Logo
. This is far from ideal and should be iterated on further in the future.
However, we needed a way to compute the X
portion of activeX
while the application was running. The answer lies in computed values:
['active' + this.state.type]: true
Now, we were able to set active states to true
or false
in real time based on the input.
Declaring and initializing multiple variables at once
While trying to use parseInt
, NaN
was being output by console.log
tests. After placing additional console.log
tests and following the flow of execution, the culprit was found:
let sum, temp = 0;
Well, then. Don’t forget to initialize your variables properly!
Updating state
This is far trickier than it initially seems. State updates may happen asynchronously.
You can use two methods to ensure state is truly updated before operating on it:
- callback function in
setState
- use
componentDidUpdate
If using a callback function to perform some operation after setState
finishes and the component is re-rended, using componentDidUpdate
is recommended by React.
Tricky state lag
American Express posed an issue several times throughout development. Since American Express credit card numbers are composed of 15 digits instead of 16 digits, maxLength
must be updated to reflect this when the corresponding prefix is entered. This goes both ways: that is, maxLength
must change from 16 to 15 when an American Express number is entered and must go from 15 to 16 when an American Express number is entered previous to a different number corresponding to one of the other issuers.
When the input corresponds to no issuer,
maxLength
defaults to 16.
The issue creates a definitive error when the length of the input is at the current maxLength
and the input is then instantly replaced by input that is of the length of the other permissible length. So, input with length 15 when maxLength
is currently 16 or input with length 16 when maxLength
is currently 15.
To reproduce this, we must select the entire input at once and paste the new input. The state, updating asynchronously, does not update in time to mitigate the error. As such, a number of things occur:
verifyNumber
is not calledgetValidMessage
text from a previous call remains renderedmaxLength
updates, but not immediately- if
maxLength
is 15 and the length of the new input is 16, the input is truncated
If the input is partially deleted or reset
is clicked, order is restored. The cause appears to be, as indicated above, that state is being operated on that shouldn’t be. The exact manner of fixing this issue is still being investigated.
See a demonstration of this issue
As an aside, componentDidUpdate
feels too busy. Best practices should be looked into for this.
Room for improvement
- manage the state lag (above)
- input validation
- insert spaces into the input as they would be on an actual credit card of the corresponding issuer
- flow