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 internationalisation
*/
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}")
}
}
)The onCustomValidation callback enables you to implement custom business rule validation after standard SDK component validation passes. This allows you to validate business-specific requirements before payment submission proceeds.
Use onCustomValidation to:
- Validate transaction amount limits or thresholds.
- Check customer eligibility for specific payment methods.
- Verify inventory availability before charging.
- Implement fraud prevention rules based on order data.
- Enforce payment policies based on customer account status.
- Validate promotional code or discount eligibility.
The SDK follows a specific validation order to ensure all checks are performed systematically:
- Standard SDK validation: Card number, CVV, expiry date, billing address, and other component fields are validated.
- Custom validation: Your
onCustomValidationcallback is invoked (if provided). - Payment submission: If both standard and custom validation pass, the payment submission proceeds.
The onCustomValidation callback is only called after all standard SDK component validations have passed successfully. If any standard validation fails, custom validation is not executed.
val cardSubmitConfig = CardSubmitComponentConfig(
// Component references
cardNumberComponent = cardNumberComponent,
cardExpiryDateComponent = cardExpiryComponent,
cardCvcComponent = cardCvcComponent,
cardHolderNameComponent = cardHolderNameComponent,
// Custom validation callback
onCustomValidation = {
Log.d("Validation", "Running custom business rule validation")
// All custom validation checks
val validationPassed = validateCustomRules()
if (!validationPassed) {
Log.w("Validation", "Custom validation failed")
}
validationPassed // Return true to proceed, false to block
},
// Other callbacks
onValidation = { results ->
// Standard validation results
val allValid = results.all { it.isValid }
Log.d("Validation", "Standard validation: ${if (allValid) "Passed" else "Failed"}")
},
onSubmitError = { error ->
Log.e("Payment", "Submission error: ${error.message}")
}
)
private fun validateCustomRules(): Boolean {
// Implement your custom validation logic
return true
}This example demonstrates a comprehensive validation flow with multiple business rules:
class PaymentValidationManager(
private val checkout: PxpCheckout,
private val customerRepository: CustomerRepository,
private val inventoryService: InventoryService
) {
fun createCardSubmitConfig(): CardSubmitComponentConfig {
return CardSubmitComponentConfig(
// Component references
cardNumberComponent = cardNumberComponent,
cardExpiryDateComponent = expiryComponent,
cardCvcComponent = cvcComponent,
// Custom validation with comprehensive business rules
onCustomValidation = {
Log.d("Payment", "Starting custom validation checks...")
try {
// Check 1: Validate transaction amount
if (!validateTransactionAmount()) {
showValidationError("Transaction amount validation failed")
return@CardSubmitComponentConfig false
}
// Check 2: Verify customer eligibility
if (!validateCustomerEligibility()) {
showValidationError("Customer not eligible for this payment method")
return@CardSubmitComponentConfig false
}
// Check 3: Validate inventory availability
if (!validateInventoryAvailability()) {
showValidationError("Items no longer available")
return@CardSubmitComponentConfig false
}
// Check 4: Verify payment limits
if (!validatePaymentLimits()) {
showValidationError("Payment exceeds allowed limits")
return@CardSubmitComponentConfig false
}
Log.d("Payment", "All custom validation checks passed")
return@CardSubmitComponentConfig true
} catch (e: Exception) {
Log.e("Payment", "Custom validation error: ${e.message}", e)
showValidationError("Validation error occurred")
return@CardSubmitComponentConfig false
}
},
// Standard validation callback
onValidation = { results ->
val failedValidations = results.filter { !it.isValid }
if (failedValidations.isNotEmpty()) {
Log.w("Payment", "Standard validation failed:")
failedValidations.forEach { result ->
Log.w("Payment", " - ${result.componentId}: ${result.errorMessage}")
}
} else {
Log.d("Payment", "Standard validation passed - proceeding to custom validation")
}
},
onSubmitError = { error ->
Log.e("Payment", "Payment submission error: ${error.message}")
handlePaymentError(error)
},
onPostAuthorisation = { result ->
when (result) {
is MerchantSubmitResult -> handlePaymentSuccess(result)
is FailedSubmitResult -> handlePaymentFailure(result)
else -> handleUnknownResult(result)
}
}
)
}
private fun validateTransactionAmount(): Boolean {
val amount = checkout.getSdkConfig().transactionData.amount
val currency = checkout.getSdkConfig().transactionData.currency
// Example: Check minimum and maximum amounts
val minAmount = 1.0
val maxAmount = 10000.0
if (amount < minAmount) {
Log.w("Payment", "Amount $amount $currency is below minimum $minAmount")
return false
}
if (amount > maxAmount) {
Log.w("Payment", "Amount $amount $currency exceeds maximum $maxAmount")
return false
}
Log.d("Payment", "Transaction amount validation passed: $amount $currency")
return true
}
private fun validateCustomerEligibility(): Boolean {
val customer = customerRepository.getCurrentCustomer()
// Example: Check customer account status
if (customer.accountStatus == AccountStatus.SUSPENDED) {
Log.w("Payment", "Customer account is suspended")
return false
}
if (customer.accountStatus == AccountStatus.PAYMENT_RESTRICTED) {
Log.w("Payment", "Customer has payment restrictions")
return false
}
// Example: Check customer tier limits
val amount = checkout.getSdkConfig().transactionData.amount
val tierLimit = customer.tier.maxTransactionAmount
if (amount > tierLimit) {
Log.w("Payment", "Amount exceeds customer tier limit: $tierLimit")
return false
}
Log.d("Payment", "Customer eligibility validation passed")
return true
}
private fun validateInventoryAvailability(): Boolean {
val cartItems = getCartItems()
// Check if all items are still in stock
val unavailableItems = cartItems.filter { item ->
!inventoryService.isAvailable(item.productId, item.quantity)
}
if (unavailableItems.isNotEmpty()) {
Log.w("Payment", "Items not available: ${unavailableItems.map { it.name }}")
return false
}
Log.d("Payment", "Inventory availability validation passed")
return true
}
private fun validatePaymentLimits(): Boolean {
val customer = customerRepository.getCurrentCustomer()
val amount = checkout.getSdkConfig().transactionData.amount
// Example: Check daily transaction limit
val todaysTotal = customerRepository.getTodaysTransactionTotal(customer.id)
val dailyLimit = 50000.0
if (todaysTotal + amount > dailyLimit) {
Log.w("Payment", "Would exceed daily limit: ${todaysTotal + amount} > $dailyLimit")
return false
}
Log.d("Payment", "Payment limits validation passed")
return true
}
private fun showValidationError(message: String) {
// Display error to user
Log.e("Payment", "Validation error: $message")
// Update UI to show error message
}
private fun handlePaymentError(error: BaseSdkException) {
// Handle payment submission error
}
private fun handlePaymentSuccess(result: MerchantSubmitResult) {
// Handle successful payment
}
private fun handlePaymentFailure(result: FailedSubmitResult) {
// Handle failed payment
}
private fun handleUnknownResult(result: SubmitResult) {
// Handle unknown result type
}
private fun getCartItems(): List<CartItem> {
// Return cart items
return emptyList()
}
}
data class CartItem(
val productId: String,
val name: String,
val quantity: Int
)
enum class AccountStatus {
ACTIVE,
SUSPENDED,
PAYMENT_RESTRICTED
}When custom validation fails, you should provide clear feedback to the user:
class ValidationErrorHandler {
fun setupCustomValidation(
cardSubmitConfig: CardSubmitComponentConfig,
errorDisplay: ErrorDisplayView
) {
cardSubmitConfig.onCustomValidation = {
val validationResult = performCustomValidation()
if (!validationResult.isValid) {
// Show user-friendly error message
errorDisplay.showError(
title = "Payment Validation Failed",
message = validationResult.userMessage,
actionButton = "Update Details"
)
// Log technical details for debugging
Log.w("Payment", "Custom validation failed: ${validationResult.technicalMessage}")
// Track validation failure for analytics
trackValidationFailure(validationResult)
false // Block submission
} else {
// Clear any previous errors
errorDisplay.clearErrors()
true // Allow submission
}
}
}
private fun performCustomValidation(): CustomValidationResult {
// Implement validation logic
return CustomValidationResult(
isValid = true,
userMessage = "",
technicalMessage = ""
)
}
private fun trackValidationFailure(result: CustomValidationResult) {
// Track analytics event
}
}
data class CustomValidationResult(
val isValid: Boolean,
val userMessage: String,
val technicalMessage: String
)
interface ErrorDisplayView {
fun showError(title: String, message: String, actionButton: String)
fun clearErrors()
}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()
}