Web SDK Payment
About
This is a JavaScript library to display a payment form on the page of the merchant who does not have PCI DSS.
Scenario of working:
- The merchant registers an order via the REST API in the payment gateway
- The received mdOrder is passed by the merchant to the page where this js-library is used
The library implements input fields that are located via an iframe on the payment gateway side
Pros:
- Secure: Merchant scripts do not handle the card data in any way, so the person managing the page content cannot gain unauthorized access.
- Simple: The script can be easily integrated into the page. Minimal customization is required to match the overall style of the site.
- No PCI DSS required on the vendor side
- Convenient: Additional fields such as
email
,language
,phone
, andjsonParams
can be passed to the server.
Cons:
- For now, this SDK is not compatible with the multiple payment attempts feature.
The library helps in collecting card data, validating and verifying it, making a payment, and automatically redirecting the buyer to the returnUrl
specified in the settings (finish page).
How to use
Connect the script
Test environment
<script src="https://uat.dskbank.bg/payment/modules/multiframe/main.js"></script>
Production environment
<script src="https://epg.dskbank.bg/payment/modules/multiframe/main.js"></script>
Preparation
First, you must create an HTML form for accepting payment. The form must contain the following blocks: #pan
, #expiry
, #cvc
, and the button #pay
. It is not necessary to use exactly these field names. You will be able to customize the names you need during initialization.
Here's an example of an HTML form that doesn't enable stored credentials:
<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>
If you use stored credentials, you need to enable an additional HTML block: #select-binding
.
Here's an example of an HTML form with stored credential fields:
<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>
You can add any additional fields to the form, such as the cardholder name, email, phone, etc. However, don't forget to pass them into the doPayment()
method later.
Web SDK initialization
Description payment form
You need to execute the constructor function new window.PaymentForm()
`.
window.PaymentForm()
can take the following properties:
PaymentForm initialization properties
By default, the apiContext is automatically taken from the link used to connect the script
modules/multiframe/main.js
.
Default value is
en
.
Default value is
true
Default value is
true
Default value is
field-container
For example:
onFormValidate: (isValid) => {
alert(isValid ? 'Congratulations!' : 'Oops! We regret.');
}
Web SDK initialization example
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 method
The destroy()
method in a Web SDK is used to remove all the resources and event listeners associated with a particular instance of the SDK. When you call the destroy()
method, it will clean up any event listeners and iframes that were created by the SDK during its lifecycle. This is useful when you no longer need the SDK instance.
The destroy()
method typically performs the following tasks:
- Removes all event listeners that were added to the SDK instance.
- Clears any iframes that were created.
Destroy method example
document.querySelector("#destroy").addEventListener("click", function () {
webSdkPaymentForm.destroy();
});
Stylization
You can define styles for the iframe container using class names:
-
{className}--focus
- field in focus -
{className}--valid
- field with valid value -
{className}--invalid
- field with invalid value
className
is set on initialization in the containerClassName
parameter.
Checkout without stored credentials
Step 1. Execute initialization method
After defining Web SDK parameters, you must call init()
.
This function returns a callback where you can, for example, hide the loader or do something else.
init()
returns a promise.
For example:
webSdkFormWithoutBindings
.init()
.then(() => {
// Script initialized successfully
document.querySelector('.payment-form-loader').classList.remove('payment-form-loader--active');
})
.then((error) => {
// Errors occurred during the initialization of the script. Further execution is not possible.
}); ;
Step 2. Pay button click handling. Execute payment method.
You can call the payment in any way that is convenient for you. To do this, call the function doPayment()
.
You do not need to send card data. The Web SDK will handle it. doPayment()
returns a promise.
The method takes the following parameters:
jsonParams: { "t-shirt-color": "black", "size": "M" }
Updates to Visa Secure Data Field Mandate
Please be advised about the following information received from IPS VISA: Twelve additional data fields are required for EMV 3DS authentication requests. Merchants must provide complete and accurate transaction data in their authentication requests. They must also ensure that the 3DS Method URL collects device data to support successful authentication, if the 3DS Method URL is provided by the issuer.
Therefore, it will be the merchant's responsibility to collect the additional fields for VISA. Please find the VBN from Visa attached.
Additional fields are passed as properties of an object in the doPayment()
argument.
Call example
webSdkFormWithoutBindings.doPayment({
// Additional params
email: 'foo@bar.com',
phone: '4420123456789',
cardholderName: 'JOHN DOE',
jsonParams: { foo: 'bar' },
})
.then((result) => {
console.log('result', result);
})
.catch((e) => {
// Error processing. For example, we show a block with an error
errorEl.innerHTML = e.message;
errorEl.classList.remove('visually-hidden');
})
.finally(() => {
// Execute in any case. For example, make the "Pay" button active again.
payButton.disabled = false;
spinnerEl.classList.add('visually-hidden');
});
Demo without stored credentials
You can register an order through the API.
This input is only for demo purposes - use Order ID value =
xxxxx-xxxxx-xxxxx-xxxxx
The entire demo code
<div class="container_demo">
<div class="about">
<form name="formRunTest">
<label for="mdOrder"> Order ID (mdOrder) <br>
<span class="label__desc">(Should come from the backend. This input only for demo)</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 requires mdOrder (pre-registered order in the gateway).<br>
You can register an order through Merchant Portal or through API. <br><br>
Or try use <code>xxxxx-xxxxx-xxxxx-xxxxx</code> if you to want check only payment form.
</div>
<div class="card-body">
<div class="col-12">
<label for="pan" class="form-label">Card number</label>
<div id="pan" class="form-control"></div>
</div>
<div class="col-6 col-expiry">
<label for="expiry" class="form-label">Expiry</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>
</div>
<!-- Additional fields for 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>
<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>Pay</span>
</button>
<!-- This buttons are needed only for demonstrate how destroy method works -->
<button class="btn btn-secondary" type="button" id="destroyFormWithoutCredentials">
<span>Destroy</span>
</button>
<button class="btn btn-secondary" type="button" id="reinitFormWithoutCredentials" style="display: none;">
<span>Reinit</span>
</button>
<div class="error my-2 text-center text-danger visually-hidden" id="error"></div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
// Function to initialize payment form to reuse it after destroy
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;
}
// Initialization of Web SDK. A required mdOrder (order ID) is needed.
initPayment(mrOrderInput.value);
}
document.formRunTest.addEventListener("submit", function (e) {
e.preventDefault();
// Initialize payment form
initPaymentForm();
});
let webSdkFormWithoutBindings;
// Array of additional field objects with field id, validation template and valid input characters
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",
},
},
},
});
// Action after initialization
webSdkFormWithoutBindings.init().then(() => {
document.querySelector(".payment-form-loader").classList.remove("payment-form-loader--active");
})
.finally(() => {
// Validation and characters replacement on fields input
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) {
// The CSS class ".additional-field-invalid" is used to display invalid fields
if (item.template.test(inputValue.value)) {
field.classList.remove("additional-field-invalid")
} else {
field.classList.add("additional-field-invalid")
}
}
})
})
})
}
// Pay handler
document.querySelector("#pay").addEventListener("click", () => {
const payButton = document.querySelector("#pay");
// Make the "Pay" button inactive to avoid double payments
payButton.disabled = true;
// Show the loader to the user
const spinnerEl = document.querySelector("#pay-spinner");
spinnerEl.classList.remove("visually-hidden");
// Hide the error container
const errorEl = document.querySelector("#error");
errorEl.classList.add("visually-hidden");
// Checking of additional fields validation before Payment
if (document.querySelectorAll('.additional-field-invalid').length) {
errorEl.innerHTML = "Form is not valid";
errorEl.classList.remove('visually-hidden');
spinnerEl.classList.add('visually-hidden');
return
}
// Start payment
webSdkFormWithoutBindings
.doPayment({
// Additional parameters
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) => {
// Execute on error
errorEl.innerHTML = e.message;
errorEl.classList.remove("visually-hidden");
})
.finally(() => {
// Execute in any case. For example, make the "Pay" button active again.
payButton.disabled = false;
spinnerEl.classList.add("visually-hidden");
});
});
// Destroy handler
document.querySelector("#destroyFormWithoutCredentials").addEventListener("click", function () {
// Remove destroy button and display reinit button for demo
this.style.display = "none";
document.querySelector("#reinitFormWithoutCredentials").style.display = "";
// Destroy web sdk form
webSdkFormWithoutBindings.destroy();
});
// Init form handler
document.querySelector("#reinitFormWithoutCredentials").addEventListener("click", function () {
// Remove reinit button and display destroy button for demo
this.style.display = "none";
document.querySelector("#destroyFormWithoutCredentials").style.display = "";
// Payment form initialization
initPaymentForm();
});
});
</script>
Checkout with stored credentials
Step 1. Execute initialization method
After defining Web SDK parameters, you must call init()
.
This function returns a callback where you can, for example, hide the loader or do something else.
init()
returns a promise.
For example:
// Initialization
webSdkFormWithBindings
.init()
.then(({ orderSession }) => {
// The `orderSession` object contains all the information about the order, including information about the bindings.
console.info("orderSession", orderSession);
// Show select binding element
document.querySelector("#select-binding-container").style.display = orderSession.bindings.length ? "" : "none";
// Fill the select with stored credentials
orderSession.bindings.forEach((binding) => {
document
.querySelector("#select-binding")
.options.add(new Option(binding.pan, binding.id));
});
// Handle select stored credential or a new card
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 {
// Selecting stored credentials with null means switching to a new card
webSdkFormWithBindings.selectBinding(null);
// Show the 'Save card' checkbox
document.querySelector("#save-card-container").style.display = "";
}
});
// When the form is ready, we can hide the loader
document.querySelector("#pay-form-loader").classList.add("visually-hidden");
})
.catch((error) => {
// Errors occurred during the initialization of the script. Further execution is not possible.
});
Step 2. Pay button click handling. Execute payment method.
You can call payment in any way that is convenient for you. To do this, call the function doPayment()
.
You do not need to send card data. WebSDK will do it. doPayment()
returns a promise.
The method takes the following parameters:
document.querySelector('#save-card').checked
jsonParams: { "t-shirt-color": "black", "size": "M" }
Apply stored credentials
To pay with a stored credential, you need to pass the selected bindingId
to the form before calling doPayment
:
webSdkFormWithBindings.selectBinding('bindingId here');
If you changed your mind and want to pay with a new card don't forget to remove bindingId
from the form:
webSdkFormWithBindings.selectBinding(null);
Call example:
webSdkFormWithBindings.doPayment({
// Additional parameters
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) => {
// Error processing. For example, we show a block with an error
errorEl.innerHTML = e.message;
errorEl.classList.remove('visually-hidden');
})
.finally(() => {
// Execute in any case. For example, make the "Pay" button active again.
payButton.disabled = false;
spinnerEl.classList.add('visually-hidden');
});
Demo with stored credentials
You can register an order through the API.
This input is only for demo purposes - use Order ID value =
xxxxx-xxxxx-xxxxx-xxxxx
The entire demo code
<div class="container_demo">
<div class="about">
<form name="formRunTest">
<label for="mdOrder"> Order ID (mdOrder) <br>
<span class="label__desc">(This input is only for demo purposes. The actual order ID should come from the backend.)</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 requires mdOrder (pre-registered order in the gateway).<br>
You can register an order through Merchant Portal or through API. <br><br>
Or try use <code>xxxxx-xxxxx-xxxxx-xxxxx</code> if you to want check only payment form.
</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">Pay with a new card</option>
</select>
</div>
<div class="col-12">
<label for="pan" class="form-label">Card number</label>
<div id="pan" class="form-control"></div>
</div>
<div class="col-6 col-expiry">
<label for="expiry" class="form-label">Expiry</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" />
Save card
</label>
<!-- Additional fields for 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="NAME 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>Pay</span>
</button>
<!-- This buttons are needed only for demonstrate how destroy method works -->
<button class="btn btn-secondary" type="button" id="destroyFormWithoutCredentials">
<span>Destroy</span>
</button>
<button class="btn btn-secondary" type="button" id="reinitFormWithoutCredentials" style="display: none;">
<span>Reinit</span>
</button>
<div class="error my-2 visually-hidden" id="error"></div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
// Function to initialize payment form to reuse it after destroy
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;
}
// Removing example placeholder
document.querySelector(".payment-form-loader").classList.remove("payment-form-loader--active");
// Adding payment form loader
document.querySelector("#pay-form-loader").classList.remove("visually-hidden");
// Initialization of Web SDK. A required mdOrder (order ID) is needed.
initPayment(mrOrderInput.value);
}
// Handler initialization for expamle input
function handleSubmit(e) {
e.preventDefault();
// Initialize payment form
initPaymentForm();
}
// Register event for example input
document.formRunTest.addEventListener("submit", handleSubmit);
let webSdkFormWithBindings;
// Array of additional field objects with field id, validation template and valid input characters
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({
// Order number (order registration happens before initialization of the form)
mdOrder: mdOrder,
// Handling form validation
onFormValidate: (isValid) => {
// For example, you can disable "pay" and "Get token" buttons if from is not valid, like this:
// const payButton = document.querySelector('#pay');
// payButton.disabled = !isValid;
},
// Context for API queries
apiContext: "/payment",
// Language - is used for localization of errors and names for placeholders.
// The language should be supported in the merchant's settings
language: "en",
// Class name for container elements containing iFrames
containerClassName: "field-container",
// Automatic switching of focus when fields are filled
autoFocus: true,
// Show payment system icon
showPanIcon: true,
// Additional styles for the payment system icon
panIconStyle: {
height: "16px",
top: "calc(50% - 8px)",
right: "8px",
},
// Field settings
fields: {
// Container element in which the iFrame with the field will be placed
pan: {
container: document.querySelector("#pan"),
},
// Card expiration date
expiry: {
container: document.querySelector("#expiry"),
},
// CVC/CVV-code
cvc: {
container: document.querySelector("#cvc"),
},
},
// Additional styles to customize the appearance of input fields within iFrames
styles: {
base: {
padding: "0px 16px",
color: "black",
fontSize: "18px",
},
disabled: {
backgroundColor: "#e9ecef",
},
invalid: {
color: "red",
},
placeholder: {
base: {
color: "gray",
},
focus: {
color: "transparent",
},
},
},
});
// Action after initialization
webSdkFormWithBindings
.init()
.then(({ orderSession }) => {
// The `orderSession` object contains all the information about the order, including information about the stored credentials.
console.info("orderSession", orderSession);
// Show the select stored credential element
document.querySelector("#select-binding-container").style.display = orderSession.bindings.length ? "" : "none";
// Fill the select with stored credentials
orderSession.bindings.forEach((binding) => {
document.querySelector("#select-binding").options.add(new Option(binding.pan, binding.id));
});
// Handle select stored credential or a new card
document.querySelector("#select-binding").addEventListener("change", function () {
const bindingId = this.value;
if (bindingId !== "new_card") {
// Set binding id
webSdkFormWithBindings.selectBinding(bindingId);
// Hide the 'Save card' checkbox
document.querySelector("#save-card-container").style.display = "none";
} else {
// Selecting stored credentials with null means switching to a new card
webSdkFormWithBindings.selectBinding(null);
// Show the 'Save card' checkbox
document.querySelector("#save-card-container").style.display = "";
}
});
// When the form is ready, we can hide the loader
document.querySelector("#pay-form-loader").classList.add("visually-hidden");
// Remove the event for the example input
document.formRunTest.removeEventListener("submit", handleSubmit);
})
.catch((error) => {
// Execute on error
const errorEl = document.querySelector("#error");
errorEl.innerHTML = e.message;
errorEl.classList.remove("visually-hidden");
})
.finally(() => {
// Validation and characters replacement on fields input
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) {
// The CSS class ".additional-field-invalid" is used to display invalid fields
if (item.template.test(inputValue.value)) {
field.classList.remove("additional-field-invalid")
} else {
field.classList.add("additional-field-invalid")
}
}
})
})
});
}
// Payment handler
document.querySelector("#pay").addEventListener("click", () => {
// Make the "Pay" button inactive to avoid double payments
const payButton = document.querySelector("#pay");
payButton.disabled = true;
// Show the loader, for the user
const spinnerEl = document.querySelector("#pay-spinner");
spinnerEl.classList.remove("visually-hidden");
// Hide the error container
const errorEl = document.querySelector("#error");
errorEl.classList.add("visually-hidden");
// Checking of additional fields validation before Payment
if (document.querySelectorAll('.additional-field-invalid').length) {
errorEl.innerHTML = "Form is not valid";
errorEl.classList.remove('visually-hidden');
spinnerEl.classList.add('visually-hidden');
return
}
// Start payment
webSdkFormWithBindings
.doPayment({
// Additional parameters
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) => {
// Here you can do something with payment result
console.log("result", result);
})
.catch((e) => {
// Execute on error
errorEl.innerHTML = e.message;
errorEl.classList.remove("visually-hidden");
})
.finally(() => {
// Execute in any case. For example, make the "Pay" button active again.
payButton.disabled = false;
spinnerEl.classList.add("visually-hidden");
});
});
// Destroy handler
document.querySelector("#destroyFormWithCredentials").addEventListener("click", function () {
// Remove destroy button and display reinit button for demo
this.style.display = "none";
document.querySelector("#reinitFormWithCredentials").style.display = "";
// Execute destroy method on web sdk form
webSdkFormWithBindings.destroy();
});
// Init payment form handler
document.querySelector("#reinitFormWithCredentials").addEventListener("click", function () {
// Remove reinit button and display destroy button for demo
this.style.display = "none";
document.querySelector("#destroyFormWithCredentials").style.display = "";
// Payment form initialization
initPaymentForm();
});
});
</script>
Web SDK in React SPA
In single page application on React it is necessary to initialize the Web SDK by executing the webSdkPaymentForm.init()
method on each initial render of the page with the Web SDK form.
On the event of resetting the page with Web SDK form (i.e. when switching to another page), it is necessary to execute the webSdkPaymentForm.destroy()
method. This is important, because only one webSDK form handler (multiframe-commutator) should remain on the page.
When returning to the page with the Web SDK form it is necessary to perform initialization again using webSdkPaymentForm.init()
method.
Note that PCI DSS compliance is required for using this library since it handles card data. Read more about PCI DSS here.
Example of React component
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://uat.dskbank.bg/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;