Web SDK Payment
Описание
JavaScript-библиотека для отображения платежной формы на странице мерчанта, у которого нет PCI DSS.
Сценарий работы:
- Мерчант регистрирует заказ через REST API в платежном шлюзе
- Полученный mdOrder мерчант передает на страницу, где используется эта js-библиотека
SDK предоставляет возможность добавлять на свою платежную страницу поля ввода платежных данных через iframe на стороне платежного шлюза.
Плюсы:
- Безопасно: Платежная страница мерчанта и скрипты мерчанта на ней не имеют доступа к платежным полям в iframe. Данные карт не могут быть собраны внешними скриптами.
- Просто: Скрипт легко интегрируется на страницу. Чтобы соответствовать общему стилю сайта, требуется минимальная кастомизация.
- Со стороны мерчанта не требуется PCI DSS.
- Удобно: На сервер могут быть переданы дополнительные поля, такие как
email
,language
,phone
иjsonParams
.
Минусы:
- На данный момент этот SDK не совместим с функцией множественных попыток оплаты.
Библиотека помогает в сборе данных карты, их валидации и проверке, совершении платежа и автоматическом перенаправлении покупателя на указанный в настройках returnUrl
(конечная страница).
Как использовать
Подключить скрипт
Тестовая среда
<script src="https://abby.rbsuat.com/payment/modules/multiframe/main.js"></script>
Боевая среда
<script src="https://ecom.alfabank.by/payment/modules/multiframe/main.js"></script>
Подготовка
Во-первых, необходимо создать HTML-форму для приема платежей. Форма должна содержать блоки #pan
, #expiry
, #cvc
и кнопку #pay
. Необязательно использовать именно эти имена полей. Вы сможете настроить нужные вам имена во время инициализации.
Вот пример HTML-формы, которая не поддерживает связки:
<div class="card-body">
<div class="col-12">
<label for="pan" class="form-label">Card number</label>
<!-- Container for card number field -->
<div id="pan" class="form-control"></div>
</div>
<div class="col-6 col-expiry">
<label for="expiry" class="form-label">Expiry</label>
<!-- Container for expiry card field -->
<div id="expiry" class="form-control"></div>
</div>
<div class="col-6 col-cvc">
<label for="cvc" class="form-label">CVC / CVV</label>
<!-- Container for CVC/CVV field -->
<div id="cvc" class="form-control"></div>
</div>
</div>
<!-- Pay button -->
<button class="btn btn-primary btn-lg" type="submit" id="pay">
<!-- Payment loader -->
<span class="spinner-border spinner-border-sm visually-hidden" id="pay-spinner"></span>
<span>Pay</span>
</button>
<!-- Container for errors -->
<div class="error my-2 text-center text-danger visually-hidden" id="error"></div>
Если вы используете связки, вам необходимо включить дополнительный блок HTML: #select-binding
.
Вот пример HTML-формы, которая поддерживает связки:
<div class="card-body">
<div class="col-12" id="select-binding-container" style="display: none">
<!-- Select for bindings -->
<select class="form-select" id="select-binding" aria-label="Default select example">
<option selected value="new_card">Pay with a new card</option>
</select>
</div>
<div class="col-12">
<label for="pan" class="form-label">Card number</label>
<!-- Container for card number field -->
<div id="pan" class="form-control"></div>
</div>
<div class="col-6 col-expiry">
<label for="expiry" class="form-label">Expiry</label>
<!-- Container for expiry card field -->
<div id="expiry" class="form-control"></div>
</div>
<div class="col-6 col-cvc">
<label for="cvc" class="form-label">CVC / CVV</label>
<!-- Container for cvc/cvv field -->
<div id="cvc" class="form-control"></div>
</div>
<label class="col-12" id="save-card-container">
<!-- Save card checkbox -->
<input class="form-check-input" type="checkbox" value="" id="save-card" />
Save card
</label>
</div>
<!-- Pay button -->
<button class="btn btn-primary btn-lg" type="submit" id="pay">
<!-- Payment loader -->
<span class="spinner-border spinner-border-sm visually-hidden" id="pay-spinner"></span>
<span>Pay</span>
</button>
<!-- Container for errors -->
<div class="error my-2 text-center text-danger visually-hidden" id="error"></div>
Вы можете добавить в форму любые дополнительные поля, такие как cardholder name (имя владельца карты), email, phone (номер телефона) и т. д. Однако не забудьте в дальнейшем передать их в метод doPayment()
.
Инициализация Web SDK
Описание платежной формы
Вам нужно выполнить функцию конструктора new window.PaymentForm()
.
window.PaymentForm()
может принимать следующие свойства:
Свойства инициализации PaymentForm
По умолчанию apiContext автоматически берется из ссылки, используемой для подключения скрипта
modules/multiframe/main.js
.
Значение по умолчанию:
en
.
Значение по умолчанию -
true
По умолчанию
true
Значение по умолчанию:
field-container
Например:
onFormValidate: (isValid) => {
alert(isValid ? 'Congratulations!' : 'Oops! We regret.');
}
Пример инициализации Web SDK
const webSdkPaymentForm = new window.PaymentForm({
// Order number (order registration happens before initialization of the form)
mdOrder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
// Name of the class that will be set for containers with frames
containerClassName: "field-container",
onFormValidate: (isValid) => {
// Handling form validation
},
// Context for API queries
apiContext: "/payment",
// Language - is used for localization of errors and placeholder names.
// The language must be supported in Merchant settings
language: "en",
// Automatically shift focus as fields are filled out
autoFocus: true,
// Show payment system icon
showPanIcon: true,
// Custom styles for payment system icon
panIconStyle: {
height: "16px",
top: "calc(50% - 8px)",
right: "8px",
},
fields: {
pan: {
container: document.querySelector("#pan"),
onFocus: (containerElement) => {
// Action when field gets focus
// (containerElement contains link to field container element)
},
onBlur: (containerElement) => {
// Action when field gets focus off it
// (containerElement contains link to field container element)
},
onValidate: (isValid, containerElement) => {
// Action when field is valid
// (isValid is true if field is valid, otherwise is false)
// (containerElement contains link to field container element)
},
},
expiry: {
container: document.querySelector("#expiry"),
// ...
},
cvc: {
container: document.querySelector("#cvc"),
// ...
},
},
// Style for input fields
styles: {
// Base state
base: {
color: "black",
},
// Focused state
focus: {
color: "blue",
},
// Disabled state
disabled: {
color: "gray",
},
// Has valid value
valid: {
color: "green",
},
// Has invalid value
invalid: {
color: "red",
},
// Style for placeholder
placeholder: {
// Base style
base: {
color: "gray",
},
// Style when focused
focus: {
color: "transparent",
},
},
},
});
Метод destroy
Метод destroy()
в Web SDK используется для удаления всех ресурсов и слушателей событий, связанных с конкретным экземпляром SDK. Когда вы вызываете метод destroy()
, он очищает все слушатели событий и контейнеры iframe, которые были созданы SDK в течение его жизненного цикла. Это полезно, когда вам больше не нужен экземпляр SDK.
Метод destroy()
обычно выполняет следующие задачи:
- Удаляет все слушатели событий, которые были добавлены в экземпляр SDK.
- Очищает все созданные контейнеры iframe.
Пример метода destroy
document.querySelector("#destroy").addEventListener("click", function () {
webSdkPaymentForm.destroy();
});
Стилизация
Вы можете определить стили для контейнера iframe, используя имена классов:
-
{className}--focus
- поле в фокусе -
{className}--valid
- поле с валидным значением -
{className}--invalid
- поле с невалидным значением
className
задаётся при инициализации в параметре containerClassName
Оплата без сохраненной карты (связки)
Шаг 1. Выполнить метод инициализации
После определения параметров Web SDK необходимо вызвать init()
.
Эта функция возвращает callback, где вы можете, например, скрыть загрузчик или сделать что-либо еще.
init()
возвращает Promise.
Например:
webSdkFormWithoutBindings
.init()
.then(() => {
// Скрипт успешно инициализирован
document
.querySelector(".payment-form-loader")
.classList.remove("payment-form-loader--active");
})
.catch((error) => {
// При инициализации скрипта возникли ошибки. Дальнейшее выполнение невозможно.
});
Шаг 2. Обработка нажатия кнопки оплаты. Выполнить метод оплаты
Чтобы выполнить оплату, вызовите функцию doPayment()
.
Отправлять данные карты не нужно, это сделает Web SDK. doPayment()
возвращает Promise.
Метод принимает следующие параметры:
jsonParams: { "t-shirt-color": "черный", "size": "M" }
Обновление мандата Visa Secure Data Field
Обратите внимание на требования IPS Visa относительно дополнительных полей данных, необходимых для запросов на аутентификацию EMV 3DS. Мерчанты должны предоставлять полные и точные данные о транзакциях в своих запросах на аутентификацию. Мерчанты также должны гарантировать, что 3DS Method URL-адрес совершает сбор данных устройства для поддержки успешной аутентификации в случае, если 3DS Method URL-адрес предоставлен эмитентом.
Таким образом, сбор дополнительных полей для VISA является ответственнотью мерчанта. Вы можете ознакомиться c полным текстом требований в Visa Secure Data Field Mandate.
Дополнительные поля передаются как свойства объекта, который является аргументом функции doPayment()
.
Пример вызова:
webSdkFormWithoutBindings
.doPayment({
// Дополнительные параметры
email: "foo@bar.com",
phone: "4420123456789",
cardholderName: "JOHN DOE",
jsonParams: { foo: "bar" },
})
.then((result) => {
console.log("result", result);
})
.catch((e) => {
// Обработка ошибок. Для примера покажем блок с ошибкой
errorEl.innerHTML = e.message;
errorEl.classList.remove("visually-hidden");
})
.finally(() => {
// Выполняется в любом случае. Например, сделать кнопку "Оплатить" снова активной.
payButton.disabled = false;
spinnerEl.classList.add("visually-hidden");
});
Демонстрация без сохраненной карты
Для рабочих целей - регистрируйте заказ через API.
Эта форма используется только для демонстрационных целей - используйте значение Order ID =
xxxxx-xxxxx-xxxxx-xxxxx
Весь демонстрационный код
<div class="container_demo">
<div class="about">
<form name="formRunTest">
<label for="mdOrder"> Order ID (mdOrder) <br>
<span class="label__desc">(Должен поступать из бэкенда. Данный ввод предназначен только для демонстрации)</span>
</label>
<div class="run-test">
<input id="mdOrder" type="text" placeholder="Paste the mdOrder registered for sandbox"/>
<button class="btn-mini" id="load" type="submit">Load</button>
</div>
</form>
</div>
<div class="payment-form">
<div class="payment-form-loader payment-form-loader--active">
Web SDK Payment требует наличия mdOrder (предварительно зарегистрированного в шлюзе заказа).<br>
Зарегистрировать заказ можно через Merchant Portal или через API. <br><br>
Или попробуйте использовать <code>xxxxx-xxxxx-xxxxx-xxxxx</code> если вы хотите получить только платежную форму.
</div>
<div class="card-body">
<div class="col-12">
<label for="pan" class="form-label">Номер карты</label>
<div id="pan" class="form-control"></div>
</div>
<div class="col-6 col-expiry">
<label for="expiry" class="form-label">Срок действия</label>
<div id="expiry" class="form-control"></div>
</div>
<div class="col-6 col-cvc">
<label for="cvc" class="form-label">CVC / CVV</label>
<div id="cvc" class="form-control"></div>
</div>
<!-- Дополнительные поля для Visa Mandatory -->
<div class="col-12 additional-field" style="display:none;">
<label for="" class="form-label">Cardholder</label>
<div id="cardholder" class="additional-field-container">
<input type="text" class="additional-field-input" placeholder="Surname"value="JOHN DOE">
</div>
</div>
<div class="col-12 additional-field" style="display:none;">
<label for="" class="form-label">Mobile phone</label>
<div id="mobile" class="additional-field-container">
<input type="tel" class="additional-field-input" placeholder="+4915112345" value="+4915112345678">
</div>
</div>
<div class="col-12 additional-field" style="display:none;">
<label for="" class="form-label">Email address</label>
<div id="email" class="additional-field-container">
<input type="text" class="additional-field-input" placeholder="address@mail" value="address@mail.com">
</div>
</div>
</div>
<button class="btn btn-primary btn-lg" type="submit" id="pay">
<span
class="spinner-border spinner-border-sm me-2 visually-hidden"
role="status"
aria-hidden="true"
id="pay-spinner">
</span>
<span>Оплатить</span>
</button>
<!-- Эти кнопки нужны только для демонстрации работы метода destroy -->
<button class="btn btn-secondary" type="button" id="destroyFormWithoutCredentials">
<span>Уничтожить</span>
</button>
<button class="btn btn-secondary" type="button" id="reinitFormWithoutCredentials" style="display: none;">
<span>Инициализировать</span>
</button>
<div class="error my-2 text-center text-danger visually-hidden" id="error"></div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
// Функция для инициализации платежной формы с возможностью повторного использования после уничтожения
function initPaymentForm() {
const mrOrderInput = document.getElementById("mdOrder");
mrOrderInput.classList.remove("invalid");
if (!/(\w+-){3,10}\w+/g.test(mrOrderInput.value)) {
mrOrderInput.classList.add("invalid");
return;
}
// Инициализация Web SDK. Необходим идентификатор заказа mdOrder.
initPayment(mrOrderInput.value);
}
document.formRunTest.addEventListener("submit", function (e) {
e.preventDefault();
// Инициализация платежной формы
initPaymentForm();
});
let webSdkFormWithoutBindings;
// Массив объектов для дополнительных полей, которые содержат id поля, его шаблон валидации и допустимые символы для ввода
const mandatoryFieldsWithoutBinding = [
{
id: '#cardholder',
template: /^[a-zA-Z '`.\-]{4,24}$/,
replace: /[^a-zA-Z ' \-`.]/g,
},
{
id: '#mobile',
template: /^\+?[1-9][0-9]{7,14}$/,
replace: /[^0-9\+]/g,
},
{
id: '#email',
template: /^[a-zA-Z0-9._-]{1,64}@([a-zA-Z0-9.-]{2,255})\.[a-zA-Z]{2,255}$/,
replace: /[^a-zA-Z0-9@._-]/g,
}
]
function initPayment(mdOrder) {
webSdkFormWithoutBindings = new window.PaymentForm({
mdOrder: mdOrder,
onFormValidate: () => {},
language: "en", // Язык (английский)
containerClassName: "field-container",
autoFocus: true,
showPanIcon: true,
panIconStyle: {
height: "16px",
top: "calc(50% - 8px)",
right: "8px",
},
fields: {
pan: {
container: document.querySelector("#pan"),
},
expiry: {
container: document.querySelector("#expiry"),
},
cvc: {
container: document.querySelector("#cvc"),
},
},
styles: {
base: {
padding: "0px 16px",
color: "black",
fontSize: "18px",
},
invalid: {
color: "red",
},
placeholder: {
base: {
color: "gray",
},
focus: {
color: "transparent",
},
},
},
});
// Действие после инициализации
webSdkFormWithoutBindings.init().then(() => {
document
.querySelector(".payment-form-loader")
.classList.remove("payment-form-loader--active");
})
.finally(() => {
// Валидация и автозамена недопустимых символов
mandatoryFieldsWithBinding.forEach(item => {
const field = document.querySelector(item.id)
field.closest(".additional-field").style.display = '';
field.addEventListener('input', () => {
let inputValue = field.querySelector('input')
inputValue.value = item.replace ? inputValue.value.replace(item.replace,'') : inputValue.value;
if (item.id.includes("#cardholder")) {
inputValue.value = inputValue.value.toUpperCase()
}
if (item.template) {
// CSS класс ".additional-field-invalid" для отображения невалидных полей
if (item.template.test(inputValue.value)) {
field.classList.remove("additional-field-invalid")
} else {
field.classList.add("additional-field-invalid")
}
}
})
})
})
}
// Обработчик "Оплатить"
document.querySelector("#pay").addEventListener("click", () => {
const payButton = document.querySelector("#pay");
// Делаем кнопку "Оплатить" неактивной, чтобы избежать двойных платежей
payButton.disabled = true;
// Показываем загрузчик пользователю
const spinnerEl = document.querySelector("#pay-spinner");
spinnerEl.classList.remove("visually-hidden");
// Скрываем контейнер с ошибкой
const errorEl = document.querySelector("#error");
errorEl.classList.add("visually-hidden");
// Валидация дополнительных полей Visa Mandatory
if (document.querySelectorAll('.additional-field-invalid').length) {
errorEl.innerHTML = "Form is not valid";
errorEl.classList.remove('visually-hidden');
spinnerEl.classList.add('visually-hidden');
return
}
// Начинаем процесс оплаты
webSdkFormWithoutBindings
.doPayment({
// Дополнительные параметры
email: document.querySelector('#email input').value,
phone: document.querySelector('#mobile input').value,
cardholderName: document.querySelector('#cardholder input').value,
jsonParams: { size: "L" },
})
.then((result) => {
console.log("result", result);
})
.catch((e) => {
// Выполняется при ошибке
errorEl.innerHTML = e.message;
errorEl.classList.remove("visually-hidden");
})
.finally(() => {
// Выполняется в любом случае, например, делаем кнопку "Оплатить" снова активной.
payButton.disabled = false;
spinnerEl.classList.add("visually-hidden");
});
});
// Обработчик "Уничтожить"
document
.querySelector("#destroyFormWithoutCredentials")
.addEventListener("click", function () {
// Убираем кнопку "Уничтожить" и отображаем кнопку "Инициализировать" для демонстрации
this.style.display = "none";
document.querySelector("#reinitFormWithoutCredentials").style.display =
"";
// Уничтожаем платежную форму Web SDK
webSdkFormWithoutBindings.destroy();
});
// Обработчик "Инициализировать форму"
document
.querySelector("#reinitFormWithoutCredentials")
.addEventListener("click", function () {
// Убираем кнопку "Инициализировать" и отображаем кнопку "Уничтожить" для демонстрации
this.style.display = "none";
document.querySelector("#destroyFormWithoutCredentials").style.display =
"";
// Инициализация платежной формы
initPaymentForm();
});
});
</script>
Оплата сохраненной картой (связкой)
Шаг 1. Выполнить метод инициализации
После определения параметров Web SDK необходимо вызвать init()
.
Эта функция возвращает callback, где вы можете, например, скрыть загрузчик или сделать что-либо еще.
init()
возвращает Promise.
Например:
// Инициализация
webSdkFormWithBindings
.init()
.then(({ orderSession }) => {
// Объект `orderSession` содержит всю информацию о заказе, включая информацию о сохраненных учетных данных (о связке).
console.info("orderSession", orderSession);
// Показать элемент выбора сохраненной карты
document.querySelector("#select-binding-container").style.display =
orderSession.bindings.length ? "" : "none";
// Заполнить выбор сохраненными учетными данными
orderSession.bindings.forEach((binding) => {
document
.querySelector("#select-binding")
.options.add(new Option(binding.pan, binding.id));
});
// Обработка выбора сохраненных учетных данных или новой карты
document
.querySelector("#select-binding")
.addEventListener("change", function () {
const bindingId = this.value;
if (bindingId !== "new_card") {
webSdkFormWithBindings.selectBinding(bindingId);
// Скрыть флажок "Сохранить карту"
document.querySelector("#save-card-container").style.display = "none";
} else {
// Выбор связки с null означает переход к новой карте
webSdkFormWithBindings.selectBinding(null);
// Показать флажок "Сохранить карту"
document.querySelector("#save-card-container").style.display = "";
}
});
// Когда форма готова, можем скрыть загрузчик
document.querySelector("#pay-form-loader").classList.add("visually-hidden");
})
.catch((error) => {
// Произошли ошибки при инициализации скрипта. Дальнейшее выполнение невозможно.
});
Шаг 2. Обработка нажатия кнопки оплаты. Выполнить метод оплаты
Чтобы выполнить оплату, вызовите функцию doPayment()
.
Отправлять данные карты не нужно, это сделает Web SDK. doPayment()
возвращает Promise.
Метод принимает следующие параметры:
document.querySelector('#save-card').checked
jsonParams: { "t-shirt-color": "черный", "size": "M" }
Применение сохраненных карт
Чтобы оплатить с помощью сохраненных учетных данных, вам необходимо передать выбранный bindingId
в форму перед вызовом doPayment
:
webSdkFormWithBindings.selectBinding('bindingId');
Если вы передумали и хотите заплатить новой картой, не забудьте удалить bindingId
из формы:
webSdkFormWithBindings.selectBinding(null);
Пример вызова:
webSdkFormWithBindings
.doPayment({
// Дополнительные параметры
email: "foo@bar.com",
phone: "4420123456789",
saveCard: document.querySelector("#save-card").checked,
cardholderName: "JOHN DOE",
jsonParams: { foo: "bar" },
})
.then((result) => {
console.log("result", result);
})
.catch((e) => {
// Обработка ошибок. Для примера покажем блок с ошибкой
errorEl.innerHTML = e.message;
errorEl.classList.remove("visually-hidden");
})
.finally(() => {
// Выполнить в любом случае. Например, сделать кнопку "Оплатить" снова активной.
payButton.disabled = false;
spinnerEl.classList.add("visually-hidden");
});
Демонстрация с сохраненной картой
Для рабочих целей - регистрируйте заказ через API.
Эта форма используется только для демонстрационных целей - используйте значение Order ID =
xxxxx-xxxxx-xxxxx-xxxxx
Весь демонстрационный код
<div class="container_demo">
<div class="about">
<form name="formRunTest">
<label for="mdOrder"> Идентификатор заказа (mdOrder) <br/>
<span class="label__desc">(Приводится только для демонстрационных целей. Идентификатор заказа должен приходить с бэкенда.)</span>
</label>
<div class="run-test">
<input id="mdOrder" type="text" placeholder="Вставьте mdOrder, зарегистрированный в Sandbox"/>
<button class="btn-mini" id="load" type="submit">Загрузить</button>
</div>
</form>
</div>
<div class="payment-form">
<div class="payment-form-loader payment-form-loader--active">
Для Web SDK Payment требуется mdOrder (предварительно зарегистрированный заказ в шлюзе).<br/>
Вы можете зарегистрировать заказ через личный кабинет или через API. <br/><br/>
Или попробуйте использовать <code>ххххх-ххххх-ххххх-ххххх</code> если вы хотите проверить только форму оплаты.
</div>
<div id="pay-form-loader" class="spinner-container visually-hidden">
<div class="spinner-border" role="status"></div>
</div>
<div class="card-body">
<div class="col-12" id="select-binding-container" style="display: none">
<select class="form-select" id="select-binding" aria-label="Default select example">
<option selected value="new_card">Оплата новой картой</option>
</select>
</div>
<div class="col-12">
<label for="pan" class="form-label">Номер карты</label>
<div id="pan" class="form-control"></div>
</div>
<div class="col-6 col-expiry">
<label for="expiry" class="form-label">Срок действия</label>
<div id="expiry" class="form-control"></div>
</div>
<div class="col-6 col-cvc">
<label for="cvc" class="form-label">CVC / CVV</label>
<div id="cvc" class="form-control"></div>
</div>
<label class="col-12" id="save-card-container">
<input class="form-check-input" type="checkbox" value="" id="save-card" />
Сохранить карту
</label>
<!--Дополнительные поля Visa Mandatory -->
<div class="col-12 additional-field" style="display:none;">
<label for="" class="form-label">Владелец карты</label>
<div id="cardholder" class="additional-field-container">
<input type="text" class="additional-field-input" placeholder="NAME SURNAME"value="JOHN DOE">
</div>
</div>
<div class="col-12 additional-field" style="display:none;">
<label for="" class="form-label">Телефон</label>
<div id="mobile" class="additional-field-container">
<input type="tel" class="additional-field-input" placeholder="+4915112345" value="+4915112345678">
</div>
</div>
<div class="col-12 additional-field" style="display:none;">
<label for="" class="form-label">Электронная почта</label>
<div id="email" class="additional-field-container">
<input type="text" class="additional-field-input" placeholder="address@mail" value="address@mail.com">
</div>
</div>
</div>
<button class="btn btn-primary btn-lg" type="submit" id="pay">
<span
class="spinner-border spinner-border-sm me-2 visually-hidden"
role="status"
aria-hidden="true"
id="pay-spinner">
</span>
<span>Оплатить</span>
</button>
<!-- Эти кнопки нужны только для демонстрации работы метода destroy -->
<button class="btn btn-secondary" type="button" id="destroyFormWithoutCredentials">
<span>Уничтожить</span>
</button>
<button class="btn btn-secondary" type="button" id="reinitFormWithoutCredentials" style="display: none;">
<span>Инициализировать</span>
</button>
<div class="error my-2 visually-hidden" id="error"></div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
// Функция для инициализации платежной формы для ее повторного использования после уничтожения
function initPaymentForm() {
const mrOrderInput = document.getElementById("mdOrder");
mrOrderInput.classList.remove("invalid");
if (!/(\w+-){3,10}\w+/g.test(mrOrderInput.value)) {
mrOrderInput.classList.add("invalid");
return;
}
// Удаление плейсхолдера
document
.querySelector(".payment-form-loader")
.classList.remove("payment-form-loader--active");
// Добавляем загрузчик платежных форм
document
.querySelector("#pay-form-loader")
.classList.remove("visually-hidden");
// Инициализация Web SDK. Требуется обязательный mdOrder (идентификатор заказа).
initPayment(mrOrderInput.value);
}
// Инициализация обработчика для тестовых данных
function handleSubmit(e) {
e.preventDefault();
// Инициализировать платежную форму
initPaymentForm();
}
// Регистрирация события для примера ввода
document.formRunTest.addEventListener("submit", handleSubmit);
let webSdkFormWithBindings;
// Массив объектов для дополнительных полей, которые содержат id поля, его шаблон валидации и допустимые символы для ввода
const mandatoryFieldsWithoutBinding = [
{
id: '#cardholder',
template: /^[a-zA-Z '`.\-]{4,24}$/,
replace: /[^a-zA-Z ' \-`.]/g,
},
{
id: '#mobile',
template: /^\+?[1-9][0-9]{7,14}$/,
replace: /[^0-9\+]/g,
},
{
id: '#email',
template: /^[a-zA-Z0-9._-]{1,64}@([a-zA-Z0-9.-]{2,255})\.[a-zA-Z]{2,255}$/,
replace: /[^a-zA-Z0-9@._-]/g,
}
]
function initPayment(mdOrder) {
webSdkFormWithBindings = new window.PaymentForm({
// Номер заказа (регистрация заказа происходит до инициализации формы)
mdOrder: mdOrder,
// Обработка валидации формы
onFormValidate: (isValid) => {
// Например, вы можете отключить кнопки «Оплатить» и «Получить токен», если форма не валидна, например:
// const payButton = document.querySelector('#pay');
// payButton.disabled = !isValid;
},
// Контекст для API вызовов
apiContext: "/payment",
// Язык используется для локализации ошибок и названий для плейсхолдеров.
// Язык должен поддерживаться в настройках мерчанта
language: "en",
// Имя класса для элементов контейнера, содержащих iframe
containerClassName: "field-container",
// Автоматическое переключение фокуса при заполнении полей
autoFocus: true,
// Показать иконку платежной системы
showPanIcon: true,
// Дополнительные стили для иконки платежной системы
panIconStyle: {
height: "16px",
top: "calc(50% - 8px)",
right: "8px",
},
// Настройки поля
fields: {
// Элемент-контейнер, в который будет помещен iframe с полем
pan: {
container: document.querySelector("#pan"),
},
// Срок действия карты
expiry: {
container: document.querySelector("#expiry"),
},
// CVC/CVV-код
cvc: {
container: document.querySelector("#cvc"),
},
},
// Дополнительные стили для настройки внешнего вида полей ввода в iframes
styles: {
base: {
padding: "0px 16px",
color: "black",
fontSize: "18px",
},
disabled: {
backgroundColor: "#e9ecef",
},
invalid: {
color: "red",
},
placeholder: {
base: {
color: "gray",
},
focus: {
color: "transparent",
},
},
},
});
// Действие после инициализации
webSdkFormWithBindings
.init()
.then(({ orderSession }) => {
// Объект `orderSession` содержит всю информацию о заказе, включая информацию о сохраненных учетных данных (о связке).
console.info("orderSession", orderSession);
// Показать выбранную связку
document.querySelector("#select-binding-container").style.display =
orderSession.bindings.length ? "" : "none";
// Заполнить выбор сохраненными учетными данными
orderSession.bindings.forEach((binding) => {
document
.querySelector("#select-binding")
.options.add(new Option(binding.pan, binding.id));
});
// Обработка выбора сохраненных учетных данных или новой карты
document
.querySelector("#select-binding")
.addEventListener("change", function () {
const bindingId = this.value;
if (bindingId !== "new_card") {
// Задать идентификатор связки
webSdkFormWithBindings.selectBinding(bindingId);
// Hide the 'Save card' checkbox
document.querySelector("#save-card-container").style.display =
"none";
} else {
// Выбор связки с null означает переход к новой карте
webSdkFormWithBindings.selectBinding(null);
// Показать флажок "Сохранить карту"
document.querySelector("#save-card-container").style.display = "";
}
});
// Когда форма готова, можем скрыть загрузчик
document
.querySelector("#pay-form-loader")
.classList.add("visually-hidden");
// Удалить событие для примера ввода
document.formRunTest.removeEventListener("submit", handleSubmit);
})
.catch((error) => {
// Выполнить при ошибке
const errorEl = document.querySelector("#error");
errorEl.innerHTML = e.message;
errorEl.classList.remove("visually-hidden");
})
.finally(() => {
// Валидация и автозамена недопустимых символов
mandatoryFieldsWithBinding.forEach(item => {
const field = document.querySelector(item.id)
field.closest(".additional-field").style.display = '';
field.addEventListener('input', () => {
let inputValue = field.querySelector('input')
inputValue.value = item.replace ? inputValue.value.replace(item.replace,'') : inputValue.value;
if (item.id.includes("#cardholder")) {
inputValue.value = inputValue.value.toUpperCase()
}
if (item.template) {
// CSS класс ".additional-field-invalid" для отображения невалидных полей
if (item.template.test(inputValue.value)) {
field.classList.remove("additional-field-invalid")
} else {
field.classList.add("additional-field-invalid")
}
}
})
})
});
}
// Обработчик платежа
document.querySelector("#pay").addEventListener("click", () => {
// Делаем кнопку "Оплатить" неактивной, чтобы избежать двойных платежей
const payButton = document.querySelector("#pay");
payButton.disabled = true;
// Показать загрузчик для пользователя
const spinnerEl = document.querySelector("#pay-spinner");
spinnerEl.classList.remove("visually-hidden");
// Скрыть контейнер ошибок
const errorEl = document.querySelector("#error");
errorEl.classList.add("visually-hidden");
// Валидация дополнительных полей Visa Mandatory
if (document.querySelectorAll('.additional-field-invalid').length) {
errorEl.innerHTML = "Form is not valid";
errorEl.classList.remove('visually-hidden');
spinnerEl.classList.add('visually-hidden');
return
}
// Начать оплату
webSdkFormWithBindings
.doPayment({
// Дополнительные параметры
email: document.querySelector('#email input').value,
phone: document.querySelector('#mobile input').value,
cardholderName: document.querySelector('#cardholder input').value,
saveCard: document.querySelector("#save-card").checked,
jsonParams: { foo: "bar" },
})
.then((result) => {
// Здесь можно что-то сделать с результатом платежа
console.log("result", result);
})
.catch((e) => {
// Выполнить при ошибке
errorEl.innerHTML = e.message;
errorEl.classList.remove("visually-hidden");
})
.finally(() => {
// Выполнить в любом случае. Например, снова сделать активной кнопку «Оплатить».
payButton.disabled = false;
spinnerEl.classList.add("visually-hidden");
});
});
// обрабочик Destroy
document
.querySelector("#destroyFormWithCredentials")
.addEventListener("click", function () {
// Удалить кнопку Destroy и отобразить кнопку повторной инициализации для демонстрации
this.style.display = "none";
document.querySelector("#reinitFormWithCredentials").style.display = "";
// Выполнить метод destroy в веб-форме SDK
webSdkFormWithBindings.destroy();
});
// Инициировать обработчик платежной формы
document
.querySelector("#reinitFormWithCredentials")
.addEventListener("click", function () {
// Удалить кнопку Reinit и отобразить кнопку Destroy для демонстрации
this.style.display = "none";
document.querySelector("#destroyFormWithCredentials").style.display = "";
// Инициализация платежной формы
initPaymentForm();
});
});
</script>
Web SDK в React SPA
При использовании Web SDK в single page application на React необходимо инициализировать Web SDK, выполнив метод webSdkPaymentForm.init()
при каждом начальном рендере страницы с формой Web SDK в SPA.
При событии сброса страницы с формой Web SDK (т.е. при переходе на другую страницу), необходимо выполнить метод webSdkPaymentForm.destroy()
. Это важно, т.к. на странице должен оставаться только один обработчик формы webSDK (multiframe-commutator).
При возврате на страницу с формой Web SDK необходимо снова провести инициализацию при помощи метода webSdkPaymentForm.init()
.
Обратите внимание, что для использования этой библиотеки требуется соответствие стандарту PCI DSS, поскольку она обрабатывает данные карты. Подробнее о PCI DSS здесь.
Пример React компонента
import { useEffect, useRef } from "react";
function addScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.setAttribute("src", src);
script.addEventListener("load", resolve);
script.addEventListener("error", reject);
document.body.appendChild(script);
});
}
function App() {
const panRef = useRef(null);
const expiryRef = useRef(null);
const cvcRef = useRef(null);
const selectBindingRef = useRef(null);
const saveCardContainerRef = useRef(null);
const payButtonRef = useRef(null);
let webSdkPaymentForm = null;
useEffect(() => {
const initPaymentForm = async () => {
await addScript(
"https://abby.rbsuat.com/payment/modules/multiframe/main.js",
);
webSdkPaymentForm = new window.PaymentForm({
mdOrder: mdOrder,
containerClassName: "field-container",
onFormValidate: (isValid) => {
// Handle form validation
},
apiContext: "/payment",
language: "en",
autoFocus: true,
showPanIcon: true,
panIconStyle: {
height: "16px",
top: "calc(50% - 8px)",
right: "8px",
},
fields: {
pan: {
container: panRef.current,
onFocus: (containerElement) => {
// Handle focus
},
onBlur: (containerElement) => {
// Handle blur
},
onValidate: (isValid, containerElement) => {
// Handle validation
},
},
expiry: {
container: expiryRef.current,
// Handle expiry setup
},
cvc: {
container: cvcRef.current,
// Handle cvc setup
},
},
styles: {
base: {
color: "black",
fontSize: "18px",
},
focus: {
color: "blue",
},
disabled: {
color: "gray",
},
valid: {
color: "green",
},
invalid: {
color: "red",
},
placeholder: {
base: {
color: "gray",
},
focus: {
color: "transparent",
},
},
},
});
webSdkPaymentForm
.init()
.then(({ orderSession }) => {
console.info("orderSession", orderSession);
if (orderSession.bindings.length) {
selectBindingRef.current.style.display = "";
} else {
selectBindingRef.current.style.display = "none";
}
if (orderSession.bindingEnabled) {
saveCardContainerRef.current.style.display = "";
} else {
saveCardContainerRef.current.style.display = "none";
}
orderSession.bindings.forEach((binding) => {
const option = new Option(binding.pan, binding.id);
selectBindingRef.current.options.add(option);
});
})
.catch(() => {
// Handle initialization error
});
};
initPaymentForm();
return () => {
if (webSdkPaymentForm) {
webSdkPaymentForm.destroy();
}
};
}, []);
const handlePayment = () => {
payButtonRef.current.disabled = true;
webSdkPaymentForm
.doPayment({})
.then((result) => {
// Handle successful payment
})
.catch((e) => {
alert("Error");
})
.finally(() => {
payButtonRef.current.disabled = false;
});
};
const handleSelectBinding = () => {
const bindingId = selectBindingRef.current.value;
if (bindingId !== "new_card") {
webSdkPaymentForm.selectBinding(bindingId);
saveCardContainerRef.current.style.display = "none";
} else {
webSdkPaymentForm.selectBinding(null);
saveCardContainerRef.current.style.display = "";
}
};
return (
<div className="container">
<div className="websdk-form">
<div className="card-body">
<div
className="col-12"
id="select-binding-container"
onChange={handleSelectBinding}
>
<select
className="form-select"
id="select-binding"
ref={selectBindingRef}
aria-label="Default select example"
>
<option value="new_card">Pay by new card</option>
</select>
</div>
<div className="col-12 input-form">
<label htmlFor="pan" className="form-label">
Card number
</label>
<div id="pan" className="form-control" ref={panRef}></div>
</div>
<div className="col-6 col-expiry input-form">
<label htmlFor="expiry" className="form-label">
Expiry
</label>
<div id="expiry" className="form-control" ref={expiryRef}></div>
</div>
<div className="col-6 col-cvc">
<label htmlFor="cvc" className="form-label">
CVC / CVV
</label>
<div id="cvc" className="form-control" ref={cvcRef}></div>
</div>
<label className="col-12" id="save-card-container">
<input
className="form-check-input me-1"
ref={saveCardContainerRef}
type="checkbox"
value=""
id="save-card"
/>
Save card
</label>
</div>
<div className="pay-control">
<button
className="btn btn-primary btn-lg"
type="submit"
id="pay"
ref={payButtonRef}
onClick={handlePayment}
>
Pay
</button>
</div>
<div
className="error my-2 text-center text-danger visually-hidden"
id="error"
></div>
</div>
</div>
);
}
export default App;