Skip to content

Data validation

Collect and validate the data entered by your customers.

Overview

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.

Validation methods

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 namegetValue()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

Validation result structure

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

Common validation scenarios

Get values before payment processing

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

Validate all components before submission

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

Built-in validation rules

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

Configuration example

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

Card brand detection and validation

The SDK automatically detects card brands based on number prefixes:

Card brandPrefix patternsLength range
Visa413-16
Mastercard51-58, 22-2716
American Express34, 3715
Discover6011, 644-649, 6516-19
JCB352816-19
Diners30, 36, 38, 3914-19
China UnionPay6216-19

Luhn algorithm validation

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

Using validation in your app

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

Built-in validation rules

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

Configuration example

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

Built-in validation rules

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 digits

Configuration example

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

Best practices

Real-time validation

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

Form submission validation

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