В этой статье я дам рекомендации по созданию платёжных форм, которые будут выгодно отличаться от форм ваших конкурентов. Каждый пункт рекомендаций будет сопровождаться примером кода. Полный пример кода, включающий адаптивную вёрстку, реализацию валидационных тултипов, и прочих мелочей опущенных для краткости в самой статье вы можете посмотреть здесь.
В рамках этой статьи мы не рассматриваем привязку формы к какому-то конкретному мерчант, мы лишь делаем её более отзывчивой.
Для создания формы мы будем использовать следующие инструменты:
- Нативный JS
- BinKing — вспомогательный сервис для создания платёжных форм: https://github.com/sdandteam/binking
- IMask — инструмент для создания масок полей ввода: https://imask.js.org/
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>на обратной<br>стороне</label>
</div>
</div>
P.S.
Пользуясь случаем, хочу обратиться к читателям:
Я ищу партнёра для развития сервиса BinKing, готов взять в долю. Сейчас планирую перевести весь сервис на английский язык, добавить прочие страны и запуститься за рубежом. От партнёра ожидаю помощи в работе с зарубежными клиентами, попомщи в переводе сайта на английский язык, попомщи в наполнении базы банков других странах. Если интересно сотрудничество, пишите.
Я занимаюсь разработкой многопользовательских веб-сервисов и мобильных приложений на заказ. Веду разработку от проектирования до выхода на рынок. Если кто-то задумал создать свой стартап и сейчас ищет того, кто будет заниматься разработкой — буду рад долгосрочному сотрудничеству.
Когда я занимаюсь разработкой веб-сервисов и мобильных приложений на заказ, мне не редко приходится подключать к работе дополнительных разработчиков, и у меня всегда большая сложность в их поиске. По-этому я решил заняться обучением программированию, чтобы сотрудничать с теми, кого я сам обучал разработке. Если у вас есть желание научиться fullstack javascript разработке и освоить Node.js, React, MongoDB, GraphQL и потом работать вместе со мной, прошу обращайтесь, договоримся об индивидуальных занятиях. В ходе занятий разработаем любой веб-сревис, который вы сами захотите.
itsoft
Это актуально банкам, а бизнес просто редиректит на страницу банка для оплаты. И это дико странно просить пользователя ввести данные карты у себя. Это нужно брать на себя ненужную ответственность.
iserdmi Автор
Ну вообще на многих популярных веб-сервисах ввод данных карты идёт прямо на странице сайта. К примеру гитхаб. Вообще BinKing для тех, кто уже принимает платежи прямо у себя на странице. То есть я не говорю, что принимать надо именно у себя, но если принимаете у себя, то BinKing поможет улучшить форму.
itsoft
Гитхаб и им подобные монстры могут это делать. Им пользователи доверяют. И есть причина - эквайрить пользователей разных стран через разные банки. Бывает даже в ресторанах терминалы разных банков. А у нас единая комиссия на интернет-эквайринг. Интересно что таи за мутки у ресторанов, что разные терминалы. Может тупо уход от налогов.
Vamp
Если приносят два счёта и два разных терминала, то вероятнее всего здесь два юр лица — ООО с лицензией на алкоголь и какой-нибудь ИП на упрощёнке для всего остального. Да, это схема оптимизации налогов.
Или ресторан имеет эквайринг с разными банками для отказоустойчивости. Если один банк упал и не принимает платежи через свой терминал, то второй банк позволит не останавливать обслуживание.
extempl
Это не значит, что так должно быть, и что это лучшая практика.
Возвращаясь к заголовку — королевская форма для приёма банковских карт та, которая не требует ввода карты. Да, я лучше буду доверять большим мастодонтам банковские данные, чем каждому третьему сайту обещающему не хранить данные карты (что, емнип, незаконно, лень искать, первый попавшийся результат тут же, на qna: https://qna.habr.com/q/76658), но сохраняющему у себя не только номер в открытом виде, но и CVV.
Нет, это определённо, не королевская форма.
iserdmi Автор
Эта статья будет полезна и мастодонтам. Не обязательно же, что такую форму будут делать именно на обычном сайте. Вполне возможно, что этим воспользуются платёжные агрегаторы или банки.
parshkov
Я бы даже сказал больше, ввод платежных данных на стороне мерчанта - это обязанность прохождения сертификации на соответствие pci dss. Поэтому да, для обычного небольшого магазина решение явно излишнее.
wscms
Статья реклама платного сервиса, согласен.
Но тем не менее, мы для использования платежных систем типа Stripe создаем такую форму у себя на сайте. Данные по API передаются в Stripe, мы храним у себя только Customer ID и Customer Card ID, они имеют вид типа cus_1q2w3e4r5t6y7u8i9o. Как для разовых платежей, так и для подписок.
Никаких других данных у себя хранить не нужно, никакой ответственности.
Так что подобные формы у себя на сайте актуальны и для мелких сайтов. Пользователи используют, но это США, не бСССР.
P.S. К автору не имею никакого отношения.