We create a royal form for accepting bank cards

Tomcat

Professional
Messages
2,656
Reputation
10
Reaction score
647
Points
113
In this article, I will give recommendations on creating payment forms that will compare favorably with the forms of your competitors. Each recommendation item will be accompanied by a code example. You can see a full code example, including adaptive layout, implementation of validation tooltypes, and other little things omitted for brevity in the article itself here .

In this article, we are not considering linking the form to any specific merchant, we are only making it more responsive.

To create the form we will use the following tools:
  1. Native JS
  2. BinKing is an auxiliary service for creating payment forms: https://github.com/sdandteam/binking
  3. IMask is a tool for creating input field masks: https://imask.js.org/
  4. Tippy - tool for creating tooltips: https://atomiks.github.io/tippyjs/

Bank logo definition​

Have you probably noticed that there are forms for accepting bank cards in which, as you enter the card number, the logo of the bank to which the bank card belongs appears?

This behavior helps to implement the JS plugin BinKing:

Code:
function initBinking () {
        binking.setDefaultOptions({
          strategy: 'api',
          apiKey: 'cbc67c2bdcead308498918a694bb8d77' // Replace it with your API key
        })
      }

      function cardNumberChangeHandler () {
        binking($cardNumberField.value, function (result) {
          // …
          if (result.formBankLogoBigSvg) {
            $bankLogo.src = result.formBankLogoBigSvg
            $bankLogo.classList.remove('binking__hide')
          } else {
            $bankLogo.classList.add('binking__hide')
          }
          // …
        })
      }

Defining bank colors​

For the beauty of the picture, I suggest you also repaint the form itself in the colors of the bank. Of course, it is also important not to forget to recolor the text color. BinKing will help us here again.

Code:
function cardNumberChangeHandler () {
        binking($cardNumberField.value, function (result) {
          // …
          $frontPanel.style.background = result.formBackgroundColor
          $frontPanel.style.color = result.formTextColor
          // …
        })
      }

Definition of the payment system logo​

Traditionally, payment forms display the logo of the payment system that issued the card. To do this, we again use the BinKing functionality. BinKing, unlike other plugins for determining the payment system, also provides the logos themselves.

Code:
function cardNumberChangeHandler () {
        binking($cardNumberField.value, function (result) {
          // …
          if (result.formBrandLogoSvg) {
            $brandLogo.src = result.formBrandLogoSvg
            $brandLogo.classList.remove('binking__hide')
          } else {
            $brandLogo.classList.add('binking__hide')
          }
          // …
        })
      }

Determining the bank of linked cards​

When entering the details of a new card, write down in your database, in addition to the card token, also aliasthe bank in the BinKing system. Then, when withdrawing linked cards, you will be able to display, in addition to the last 4 digits and the payment system logo, also the bank logo, which will greatly simplify the user’s life. Moreover, BinKing issues both full-size bank logos and bank emblems separately.

Code:
function showSavedCards () {
        if (savedCards.length) {
          var banksAliases = savedCards.map(function (card) {
            return card.bankAlias
          })
          binking.getBanks(banksAliases, function (result) {
            savedCardsBanks = result
            var savedCardsListHtml = savedCards.reduce(function (acc, card, i) {
              if (result[i]) {
                return acc += '<div class="binking__card" data-index="' + i + '">' +
                '<img class="binking__card-bank-logo" src="' + result[i].bankLogoSmallOriginalSvg + '" />' +
                '<img class="binking__card-brand-logo" src="' + binking.getBrandLogo(card.brandAlias) + '" />' +
                '<div class="binking__card-last4">...' + card.last4 + '</div>' +
                '<div class="binking__card-exp">' + card.expMonth + '/' + card.expYear + '</div>' +
                '</div>'
              }
              return acc += '<div class="binking__card" data-index="' + i + '">' +
                '<img class="binking__card-brand-logo" src="' + binking.getBrandLogo(card.brandAlias) + '" />' +
                '<div class="binking__card-last4">... ' + card.last4 + '</div>' +
                '<div class="binking__card-exp">' + card.expMonth + '/' + card.expYear + '</div>' +
                '</div>'
            }, '') // вывод карты, для которой не был найден банк
            $сardsList.innerHTML = savedCardsListHtml + $сardsList.innerHTML
          })
        }
      }

Automatic focus of the first field​

It is convenient when the cursor is already positioned in the first field, that is, in the field for entering a bank card. It's easy, just a couple of lines of code:

Code:
var $cardNumberField = document.querySelector('.binking__number-field')
      $cardNumberField.focus()

Automatically translate the cursor​

It is convenient for the user when the cursor automatically moves between fields as data is entered. The biggest trick is to move the cursor out of the card input field in a timely manner. The trouble is that not all card numbers consist of 16 digits. The cursor should be moved if and only if the entered characters are not less than the minimum length of the card, and when there are no errors in the card number according to the Luhn algorithm (an algorithm that allows you to determine whether the card number contains typos).

Code:
function cardNumberChangeHandler () {
        binking($cardNumberField.value, function (result) {
          // …
          var validationResult = validate()
          var isFulfilled = result.cardNumberNormalized.length >= result.cardNumberMinLength
          var isChanged = prevNumberValue !== $cardNumberField.value
          if (isChanged && isFulfilled) {
            if (validationResult.errors.cardNumber) {
              cardNumberTouched = true
              validate()
            } else {
              $monthField.focus()
            }
          }
          prevNumberValue = $cardNumberField.value
        })
      }

      function monthChangeHandler () {
        var validationResult = validate()
        if (prevMonthValue !== $monthField.value && $monthField.value.length >= 2) {
          if (validationResult.errors.month) {
            monthTouched = true
            validate()
          } else {
            $yearField.focus()
          }
        }
        prevMonthValue = $monthField.value
      }

      function yearChangeHandler () {
        var validationResult = validate()
        if (prevYearValue !== $yearField.value && $yearField.value.length >= 2) {
          if (validationResult.errors.year) {
            yearTouched = true
            validate()
          } else {
            $codeField.focus()
          }
        }
        prevYearValue = $yearField.value
      }

Validation of form fields​

To validate form fields, we use the validate method from BinKing. The validator will make sure that there are no typos in the card number, that the card expiration date is in the future and not in the past, will check that the fields are filled in, etc.: https://github.com/union-1/binking#%D0% B2%D0%B0%D0%BB%D0%B8%D0%B4%D0%B0%D1%86%D0%B8%D1%8F

Code:
function validate () {
        var validationResult = binking.validate($cardNumberField.value, $monthField.value, $yearField.value, $codeField.value)
        if (validationResult.errors.cardNumber && cardNumberTouched) {
          cardNumberTip.setContent(validationResult.errors.cardNumber.message)
          cardNumberTip.show()
        } else {
          cardNumberTip.hide()
        }
        var monthHasError = validationResult.errors.month && monthTouched
        if (monthHasError) {
          monthTip.setContent(validationResult.errors.month.message)
          monthTip.show()
        } else {
          monthTip.hide()
        }
        if (!monthHasError && validationResult.errors.year && yearTouched) {
          yearTip.setContent(validationResult.errors.year.message)
          yearTip.show()
        } else {
          yearTip.hide()
        }
        if (validationResult.errors.code && codeTouched) {
          codeTip.setContent(validationResult.errors.code.message)
          codeTip.show()
        } else {
          codeTip.hide()
        }
        return validationResult
      }

Form field masks​

Let's make it so that only numbers can be entered into our fields, the card number is neatly separated by spaces, and a number greater than 12 cannot be entered in the month field.

Code:
function initMasks () {
        cardNumberMask = IMask($cardNumberField, {
          mask: binking.defaultResult.cardNumberMask
        })
        monthMask = IMask($monthField, {
          mask: IMask.MaskedRange,
          from: 1,
          to: 12,
          maxLength: 2,
          autofix: true
        })
        yearMask = IMask($yearField, {
          mask: '00'
        })
        codeMask = IMask($codeField, {
          mask: '0000'
        })
      }

Showing the bank's phone number if a payment is declined​

If the payment is rejected by the bank, that is, the transfer error is not on our side, then in order to reduce the load on your support department, show the user a clear message indicating the name of the bank and the bank’s phone number. All this can again be done thanks to BinKing.

Code:
function cardNumberChangeHandler () {
        binking($cardNumberField.value, function (result) {
          newCardInfo = result
          // …
        })
      }

      function formSubmitHandler (e) {
        // …
        var bankInfo = selectedCardIndex !== null ? savedCardsBanks[selectedCardIndex] : newCardInfo || null
        $error.innerHTML = bankInfo && bankInfo.bankPhone
         
? 'Your bank declined the transaction on the specified card. Call '+bankInfo.bankLocalName+' at '+bankInfo.bankPhone+' to resolve the issue.'

           : 'Your bank declined the transaction on the specified card.'
        // …

Logos that inspire confidence​

It is customary to place logos that inspire confidence next to the form. So that you don’t have to look for them yourself, here are these logos in svg format.

Code:
<div class="binking__trust-logos">
        <img class="binking__trust-logo" src="https://static.binking.io/trust-logos/secure-connection.svg" alt="">
        <img class="binking__trust-logo" src="https://static.binking.io/trust-logos/mastercard.svg" alt="">
        <img class="binking__trust-logo" src="https://static.binking.io/trust-logos/mir.svg" alt="">
        <img class="binking__trust-logo" src="https://static.binking.io/trust-logos/visa.svg" alt="">
        <img class="binking__trust-logo" src="https://static.binking.io/trust-logos/pci-dss.svg" alt="">
      </div>

Correct keyboard layout​

On mobile phones, it is possible to specify what the keyboard will be displayed when focusing on a particular field. Let's make the keyboard drop out to enter numbers. To do this you need to specify the attributesinputmode="numeric" pattern="[0-9]*"

Recognition of card input fields​

Some users have payment card data saved in their browser. For automatic field recognition to work in your form, you must specify the correct attributes nameandautocomplete

Code:
<div class="binking__panel binking__front-panel">
            <img class="binking__form-bank-logo binking__hide">
            <img class="binking__form-brand-logo binking__hide">
            <div class="binking__front-fields">
              <input name="cardnumber" autocomplete="cc-number" inputmode="numeric" pattern="[0-9 ]*" class="binking__field binking__number-field" type="text" placeholder="0000 0000 0000 0000">
              <label class="binking__label binking__date-label">Действует до</label>
              <input name="ccmonth" autocomplete="cc-exp-month" inputmode="numeric" pattern="[0-9]*" class="binking__field binking__month-field binking__date-field" type="text" placeholder="MM">
              <input name="ccyear" autocomplete="cc-exp-year" inputmode="numeric" pattern="[0-9]*" class="binking__field binking__year-field binking__date-field" type="text" placeholder="YY">
            </div>
          </div>
          <div class="binking__panel binking__back-panel">
            <input name="cvc" autocomplete="cc-csc" inputmode="numeric" pattern="[0-9]*" class="binking__field binking__code-field" type="password">
            <label class="binking__label binking__code-label">Код<br>на&nbsp;обратной<br>стороне</label>
          </div>
        </div>

P.S.​

I would like to take this opportunity to address the readers:
  1. I am looking for a partner to develop the BinKing service, I am ready to take a share. Now I plan to translate the entire service into English, add other countries and launch abroad. I expect help from my partner in working with foreign clients, help in translating the site into English, help in filling the database of banks in other countries. If you are interested in cooperation, write.
  2. I develop custom multi-user web services and mobile applications. I lead development from design to market entry. If someone is planning to create their own startup and is now looking for someone who will do development, I will be glad for long-term cooperation.
  3. When I develop custom web services and mobile applications, I often have to involve additional developers in the work, and it is always very difficult for me to find them. Therefore, I decided to start teaching programming in order to collaborate with those whom I myself taught development. If you have a desire to learn fullstack javascript development and master Node.js, React, MongoDB, GraphQL and then work with me, please contact us and we can arrange individual lessons. During the classes, we will develop any web service that you want.
 
Top