В этой статье я дам рекомендации по созданию платёжных форм, которые будут выгодно отличаться от форм ваших конкурентов. Каждый пункт рекомендаций будет сопровождаться примером кода. Полный пример кода, включающий адаптивную вёрстку, реализацию валидационных тултипов, и прочих мелочей опущенных для краткости в самой статье вы можете посмотреть здесь.


В рамках этой статьи мы не рассматриваем привязку формы к какому-то конкретному мерчант, мы лишь делаем её более отзывчивой.


Для создания формы мы будем использовать следующие инструменты:


  1. Нативный JS
  2. BinKing — вспомогательный сервис для создания платёжных форм: https://github.com/sdandteam/binking
  3. IMask — инструмент для создания масок полей ввода: https://imask.js.org/
  4. Tippy — инструмент для создания тултипов: https://atomiks.github.io/tippyjs/



Определение логотипа банка


Вы наверное замечали, что существуют такие формы для приёма банковских карт, в которых, по мере ввода номера карты, появляется логотип банка, которому принадлежит банковская карта?


Такое поведение помогает реализовать JS плагин BinKing:


      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')
          }
          // …
        })
      }

Определение цветов банка


Для красоты картины предлагаю вам также перекрашивать саму форму в цвета банка. Разумеется важно также не забыть и перекрасить цвет текста. Здесь нам опять же поможет BinKing.


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

Определение логотипа платёжной системы


Традиционно формы оплаты отображают логотип платёжной системы выпустившей карту. Для этого опять же используем функционал BinKing. BinKing, в отличие от других плагинов для определения платёжной системы, предоставляет и сами логотипы.


      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')
          }
          // …
        })
      }

Определение банка привязанных карт


При вводе данных новой карты, записывайте себе в базу данных кроме токена карты ещё и alias банка в системе BinKing. Тогда при выводе привязанных карт вы сможете вывести кроме последних 4 цифр и логотипа платёжной системы ещё и логотип банка, что сильно упростит жизнь пользователю. Причём BinKing выдаёт как полноразмерные логотипы банков, так и эмблемы банков отдельно.


      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
          })
        }
      }

Автоматический фокус первого поля


Удобно, когда курсор уже установлен в первое поле, то есть в поле для ввода банковской карты. Это легко, достаточно пары строк кода:


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

Автоматически перевод курсора


Пользователю удобно, когда курсор автоматически перемещается между полями по мере ввода данных. Самая большая хитрость состоит в том, чтобы своевременно перевести курсор из поля для ввода карты. Беда в том, что не все номера карт состоят из 16 цифр. Переводить курсор следует тогда и только тогда, когда введено символов не меньше минимальной длины карты, и когда в номере карты нету ошибок согласно алгоритму Луна (алгоритм позволяющий определить содержатся ли в номере карты опечатки).


      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
      }

Валидация полей формы


Для валидация полей формы мы используем метод validate от BinKing. Валидатор позаботится о том, чтобы в номере карты не было опечаток, чтобы дата срока истечения карты была в будущем, а не в прошлом, проверит заполненность полей и прочее: https://github.com/union-1/binking#%D0%B2%D0%B0%D0%BB%D0%B8%D0%B4%D0%B0%D1%86%D0%B8%D1%8F


      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
      }

Маски полей формы


Давайте сделаем так, чтобы в наши поля можно было вводить только цифры, номер карты аккуратно разделялся пробелами, а в поле месяца нельзя было ввести число большее 12.


      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'
        })
      }

Показ телефона банка в случае отклонения платежа


Если платёж отклоняется банком, то есть ошибка перевода не наш вашей стороне, то с целью снижения нагрузки на ваш отдел поддержки, покажите пользователю понятно сообщение, указав название банка и номер телефона банка. Всё это опять же можно сделать благодаря BinKing.


      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
          ? 'Ваш банк отклонил операцию по указанной карте. Позвоните в ' + bankInfo.bankLocalName + ' по номеру ' + bankInfo.bankPhone + ', чтобы устранить причину.'
          : 'Ваш банк отклонил операцию по указанной карте.'
        // …

Логотипы вызывающие доверие


Принято размещать рядом с формой логотипы вызывающие доверия. Чтобы вам самостоятельно не пришлось их искать, вот вам эти логотипы в формате svg.


      <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>

Правильная раскладка клавиатуры


На мобильных телефонах возможно указать то, какой будет отображаемая клавиатура при фокусе на том или ином поле. Давайте сделаем так, чтобы выпадала клавиатура для ввода чисел. Для этого необходимо указать атрибуты inputmode="numeric" pattern="[0-9]*"


Распознавание полей для ввода карты


У некоторых пользователей сохранены данные платёжных карт в браузере. Чтобы в вашей форме работало автоматическое распознавание полей необходимо указать правильные атрибуты name и autocomplete


          <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.


Пользуясь случаем, хочу обратиться к читателям:


  1. Я ищу партнёра для развития сервиса BinKing, готов взять в долю. Сейчас планирую перевести весь сервис на английский язык, добавить прочие страны и запуститься за рубежом. От партнёра ожидаю помощи в работе с зарубежными клиентами, попомщи в переводе сайта на английский язык, попомщи в наполнении базы банков других странах. Если интересно сотрудничество, пишите.


  2. Я занимаюсь разработкой многопользовательских веб-сервисов и мобильных приложений на заказ. Веду разработку от проектирования до выхода на рынок. Если кто-то задумал создать свой стартап и сейчас ищет того, кто будет заниматься разработкой — буду рад долгосрочному сотрудничеству.


  3. Когда я занимаюсь разработкой веб-сервисов и мобильных приложений на заказ, мне не редко приходится подключать к работе дополнительных разработчиков, и у меня всегда большая сложность в их поиске. По-этому я решил заняться обучением программированию, чтобы сотрудничать с теми, кого я сам обучал разработке. Если у вас есть желание научиться fullstack javascript разработке и освоить Node.js, React, MongoDB, GraphQL и потом работать вместе со мной, прошу обращайтесь, договоримся об индивидуальных занятиях. В ходе занятий разработаем любой веб-сревис, который вы сами захотите.