Collect and validate the data entered by your customers.
By validating data prior to transaction initiation, you can:
- Reduce the risk of fraudulent payments.
- Offer a better user experience, by providing clear feedback that helps customers complete forms correctly.
- Ensure you meet PCI and regulatory requirements.
There are three key validation methods:
getValue(): Retrieves current field values synchronously.validate(): Validates field data and returns results.validateWhenSubmit(): Validates component data during submission.
Validation can happen:
- Automatically on user input (when field is touched and value changes).
- When
submit()is called. - When you call validation methods directly.
The following table shows the compatibility of these methods with the different components.
| Component name | getValue() | validate() | validateWhenSubmit() |
|---|---|---|---|
| Address | |||
| Billing address | |||
| Card brand selector | Returns null | ||
| Card consent | Returns Boolean | ||
| Card CVC | |||
| Card expiry date | |||
| Cardholder name | |||
| Card number | |||
| Card-on-file | Returns null | ||
| Card submit | Returns null | ||
| Click-once | Returns null | ||
| Country selection | |||
| Dynamic card image | Returns null | ||
| New card | Returns null | ||
| Postcode | |||
| Pre-fill billing address checkbox | Returns Boolean |
data class ValidationResult(
/**
* Whether the validation passed
*/
val isValid: Boolean,
/**
* Error message if validation failed
*/
val errorMessage: String? = null,
/**
* Error code if validation failed (e.g., "CN01", "CN02")
* Used for specific error handling and internationalization
*/
val errorCode: String? = null,
/**
* Component ID for tracking which component the validation result belongs to
*/
val componentId: String? = null,
/**
* Detailed validation errors (for complex validations like Kount)
* Maps field names to validation errors
*/
val validationErrors: Map<String, ValidationFieldError>? = null
) {
companion object {
fun success(): ValidationResult {
return ValidationResult(isValid = true)
}
fun error(errorMessage: String, componentId: String? = null): ValidationResult {
return ValidationResult(
isValid = false,
errorMessage = errorMessage,
componentId = componentId
)
}
fun error(errorCode: String, errorMessage: String, componentId: String? = null): ValidationResult {
return ValidationResult(
isValid = false,
errorMessage = errorMessage,
errorCode = errorCode,
componentId = componentId
)
}
}
}Use getValue() to collect form data before processing payments.
class PaymentValidationManager(
private val cardNumberComponent: CardNumberComponent,
private val expiryDateComponent: CardExpiryDateComponent,
private val cvcComponent: CardCvcComponent,
private val holderNameComponent: CardHolderNameComponent,
private val billingAddressComponent: BillingAddressComponent?
) {
suspend fun processPayment(): PaymentResult {
// Get individual card field values
val cardNumber = cardNumberComponent.getValue()
val expiryDate = expiryDateComponent.getValue()
val cvc = cvcComponent.getValue()
val holderName = holderNameComponent.getValue()
// Get complex component values
val addressData = billingAddressComponent?.getValue()
Log.d("Payment", "Payment data collected: cardNumber=${cardNumber?.let { "****${it.takeLast(4)}" }}, expiryDate=$expiryDate")
// Validate all collected data
if (!validateCollectedData(cardNumber, expiryDate, cvc, holderName, addressData)) {
return PaymentResult.ValidationFailed("Invalid payment data")
}
// Submit payment with collected data
return submitPayment(cardNumber, expiryDate, cvc, holderName, addressData)
}
private fun validateCollectedData(
cardNumber: String?,
expiryDate: String?,
cvc: String?,
holderName: String?,
addressData: AddressData?
): Boolean {
return cardNumber?.isNotEmpty() == true &&
expiryDate?.isNotEmpty() == true &&
cvc?.isNotEmpty() == true &&
holderName?.isNotEmpty() == true
}
}Use validate() to check individual components and validateWhenSubmit() for submission validation.
class FormValidationExample {
fun validateAllFieldsBeforeSubmission(
cardNumber: CardNumberComponent,
expiry: CardExpiryDateComponent,
cvc: CardCvcComponent,
holderName: CardHolderNameComponent
): Boolean {
val results = mutableListOf<ValidationResult>()
// Validate individual components
results.addAll(cardNumber.validate())
results.addAll(expiry.validate())
results.addAll(cvc.validate())
results.addAll(holderName.validate())
// Check if all validations passed
val isAllValid = results.all { it.isValid }
if (!isAllValid) {
Log.w("Validation", "Form validation failed:")
results.filter { !it.isValid }.forEach { result ->
Log.w("Validation", "- ${result.errorCode}: ${result.errorMessage}")
}
}
return isAllValid
}
fun validateForSubmission(components: List<Component<*>>): List<ValidationResult> {
return components.flatMap { component ->
component.validateWhenSubmit()
}
}
}Card number validation follows this priority order:
- Required field validation (CN01): The field can't be empty.
- Format validation (CN02): The field must contain only numeric characters.
- Length validation (CN03): The field must meet card brand length requirements.
- Brand detection (CN04): The card brand must be recognisable.
- Brand acceptance (CN05): The card brand must be in accepted list.
- Luhn algorithm (CN06): The field must pass checksum validation.
data class CardNumberValidations(
/**
* Error message when card number is required but not provided - REQUIRED (CN01)
*/
var required: String = "Card number is required",
/**
* Error message when card number contains non-numeric characters - INVALID (CN02)
*/
var invalid: String = "Please enter a valid card number",
/**
* Error message when card number is too short for detected card brand - TOO_SHORT (CN03)
*/
var tooShort: String = "Card number is too short",
/**
* Error message when card type is not recognized - TYPE_NOT_RECOGNIZED (CN04)
*/
var typeNotRecognized: String = "Card type not recognized",
/**
* Error message when card brand is not supported - TYPE_NOT_SUPPORTED (CN05)
*/
var typeNotSupported: String = "Card type is not supported",
/**
* Error message when card number fails the Luhn check algorithm - LUHN_FAILED (CN06)
*/
var luhnFailed: String = "Please enter a valid card number"
)val cardNumberConfig = CardNumberComponentConfig(
label = "Card Number",
placeholder = "1234 5678 9012 3456",
validationEnabled = true,
formatCardNumber = true,
acceptedCardBrands = listOf(CardBrand.VISA, CardBrand.MASTERCARD, CardBrand.AMEX),
validations = CardNumberValidations(
required = "Please enter your card number",
invalid = "Please enter a valid card number",
tooShort = "Your card number is incomplete",
typeNotRecognized = "Please enter a valid card number",
typeNotSupported = "This card type is not accepted",
luhnFailed = "Please check your card number"
),
// Callbacks
onCardBrandChanged = { cardBrand ->
Log.d("CardNumber", "Card brand detected: $cardBrand")
},
onCardBrandCannotRecognised = {
Log.w("CardNumber", "Unable to recognise card brand")
},
// Validation callbacks
onValidationPassed = { results ->
Log.d("CardNumber", "Validation passed")
},
onValidationFailed = { results ->
results.forEach { result ->
Log.w("CardNumber", "Validation failed: ${result.errorMessage}")
}
}
)The SDK automatically detects card brands based on number prefixes:
| Card brand | Prefix patterns | Length range |
|---|---|---|
| Visa | 4 | 13-16 |
| Mastercard | 51-58, 22-27 | 16 |
| American Express | 34, 37 | 15 |
| Discover | 6011, 644-649, 65 | 16-19 |
| JCB | 3528 | 16-19 |
| Diners | 30, 36, 38, 39 | 14-19 |
| China UnionPay | 62 | 16-19 |
The SDK uses the Luhn algorithm to validate card number checksums:
object LuhnAlgorithm {
fun isValid(cardNumber: String): Boolean {
if (cardNumber.isEmpty()) return false
val digits = cardNumber.map { it.toString().toInt() }
val checksum = digits.reversed()
.mapIndexed { index, digit ->
if (index % 2 == 1) {
val doubled = digit * 2
if (doubled > 9) doubled - 9 else doubled
} else {
digit
}
}
.sum()
return checksum % 10 == 0
}
}class PaymentValidator {
fun validateCardData(
cardNumber: String,
expiryMonth: String,
expiryYear: String,
cvc: String
): ValidationResult {
// Validate card number
val cardValidation = validateCardNumber(cardNumber)
if (!cardValidation.isValid) {
return cardValidation
}
// Validate expiry date
val expiryValidation = validateExpiryDate(expiryMonth, expiryYear)
if (!expiryValidation.isValid) {
return expiryValidation
}
// Validate CVC
val cvcValidation = validateCvc(cvc)
if (!cvcValidation.isValid) {
return cvcValidation
}
return ValidationResult.success()
}
private fun validateCardNumber(cardNumber: String): ValidationResult {
val digitsOnly = cardNumber.replace("\\s".toRegex(), "")
// Check length
if (!digitsOnly.matches("^[0-9]{13,19}$".toRegex())) {
return ValidationResult.error(
errorCode = "CN02",
errorMessage = "Please enter a valid card number"
)
}
// Luhn algorithm check
if (!LuhnAlgorithm.isValid(digitsOnly)) {
return ValidationResult.error(
errorCode = "CN06",
errorMessage = "Please check your card number"
)
}
return ValidationResult.success()
}
private fun validateExpiryDate(month: String, year: String): ValidationResult {
val monthInt = month.toIntOrNull() ?: return ValidationResult.error(
errorCode = "ED01",
errorMessage = "Invalid expiry month"
)
val yearInt = year.toIntOrNull() ?: return ValidationResult.error(
errorCode = "ED02",
errorMessage = "Invalid expiry year"
)
if (monthInt < 1 || monthInt > 12) {
return ValidationResult.error(
errorCode = "ED01",
errorMessage = "Expiry month must be between 1 and 12"
)
}
val currentYear = Calendar.getInstance().get(Calendar.YEAR)
val currentMonth = Calendar.getInstance().get(Calendar.MONTH) + 1
val fullYear = if (yearInt < 100) 2000 + yearInt else yearInt
if (fullYear < currentYear || (fullYear == currentYear && monthInt < currentMonth)) {
return ValidationResult.error(
errorCode = "ED03",
errorMessage = "Card has expired"
)
}
return ValidationResult.success()
}
private fun validateCvc(cvc: String): ValidationResult {
if (cvc.isEmpty()) {
return ValidationResult.error(
errorCode = "CVC01",
errorMessage = "CVC is required"
)
}
if (!cvc.matches("^[0-9]{3,4}$".toRegex())) {
return ValidationResult.error(
errorCode = "CVC02",
errorMessage = "CVC must be 3 or 4 digits"
)
}
return ValidationResult.success()
}
}Expiry date validation uses error codes ED01-ED05:
- Format validation (ED01): The field must contain only numeric characters.
- Month validation (ED02): The month must be between 01-12.
- Pattern validation (ED03): The field must match MM/YY format.
- Expiry check (ED04): The date can't be in the past.
- Required validation (ED05): The field can't be empty.
data class CardExpiryDateValidations(
/** Invalid format - non-numeric characters (ED01) */
var invalidFormat: String = "Expiry date must contain only digits",
/** Invalid month - not between 01-12 (ED02) */
var invalidMonth: String = "Invalid month. Please enter a month between 01 and 12",
/** Invalid format pattern (ED03) */
var invalidFormatPattern: String = "Invalid expiry date format. Please use ",
/** Card expired (ED04) */
var expired: String = "The card expiry date you entered has already passed. Please enter a valid expiration date",
/** Required field empty (ED05) */
var required: String = "Please enter expiry date"
)val expiryDateConfig = CardExpiryDateComponentConfig(
label = "Expiry Date",
formatOptions = ExpiryDateFormat.SHORT_SLASH, // MM/YY
validations = CardExpiryDateValidations(
required = "Please provide your card expiry date",
invalidFormat = "Only numbers are allowed (no letters or symbols)",
expired = "Your card expiry date has passed",
invalidMonth = "Month must be between 01-12"
),
// Validation callbacks
onValidationPassed = {
Log.d("ExpiryDate", "Validation passed")
},
onValidationFailed = { results ->
results.forEach { result ->
Log.w("ExpiryDate", "Validation failed: ${result.errorMessage}")
}
}
)CVC validation includes these checks:
- Required validation: The field cannot be empty.
- Format validation: The field must contain only numeric characters.
- Length validation: The field must be 3-4 digits depending on the card brand.
// CVC validation is handled automatically by the component
// The length depends on the detected card brand:
// - Visa, Mastercard, Discover: 3 digits
// - American Express: 4 digitsval cvcConfig = CardCvcComponentConfig(
label = "CVC",
placeholder = "123",
// Validation callbacks
onValidationPassed = {
Log.d("CVC", "CVC validation passed")
},
onValidationFailed = { results ->
results.forEach { result ->
Log.w("CVC", "CVC validation failed: ${result.errorMessage}")
}
}
)class FormValidationManager {
fun setupRealTimeValidation(
cardNumber: CardNumberComponent,
expiry: CardExpiryDateComponent,
cvc: CardCvcComponent
) {
// Validate as user types
cardNumber.config.onValidationFailed = { results ->
showFieldError(cardNumber, results.firstOrNull()?.errorMessage)
}
cardNumber.config.onValidationPassed = {
hideFieldError(cardNumber)
}
// Similar for other components
expiry.config.onValidationFailed = { results ->
showFieldError(expiry, results.firstOrNull()?.errorMessage)
}
cvc.config.onValidationFailed = { results ->
showFieldError(cvc, results.firstOrNull()?.errorMessage)
}
}
private fun showFieldError(component: Component<*>, message: String?) {
// Update UI to show error state
Log.w("Validation", "Field ${component.componentType}: $message")
}
private fun hideFieldError(component: Component<*>) {
// Update UI to hide error state
Log.d("Validation", "Field ${component.componentType} is valid")
}
}suspend fun validateAndSubmitPayment(
cardNumber: CardNumberComponent,
expiry: CardExpiryDateComponent,
cvc: CardCvcComponent,
holderName: CardHolderNameComponent
): PaymentResult {
// Validate all components before submission
val allResults = mutableListOf<ValidationResult>()
allResults.addAll(cardNumber.validateWhenSubmit())
allResults.addAll(expiry.validateWhenSubmit())
allResults.addAll(cvc.validateWhenSubmit())
allResults.addAll(holderName.validateWhenSubmit())
val failedResults = allResults.filter { !it.isValid }
if (failedResults.isNotEmpty()) {
Log.w("Payment", "Form validation failed:")
failedResults.forEach { result ->
Log.w("Payment", "- ${result.errorCode}: ${result.errorMessage}")
}
return PaymentResult.ValidationFailed(failedResults)
}
// All validations passed, proceed with payment
return processPayment()
}