Skip to content

Data validation

Learn about built-in validation and implement additional scenarios for Android.

Overview

The PayPal component includes comprehensive validation to ensure data integrity and compliance with PayPal's requirements. All built-in validation is performed before creating orders. If it fails, the SDK will trigger the onSubmitError callback with detailed error information.

You can also easily build custom validation, depending on your business needs.

Built-in validation

By default, the PayPal component validates that:

  • Fields marked as required are provided.
  • Maximum length constraints are respected.
  • All email, currency code, country code, and date fields are formatted properly.
  • Valid values are provided for every enum.

Error codes

The PayPal component returns structured error codes for different validation failures:

Error code Description
REQUIRED_FIELDAt least one required field is missing.
MAX_LENGTH_EXCEEDEDAt least one field exceeds its maximum length.
INVALID_LENGTHAt least one field has the incorrect length.
INVALID_EMAIL_FORMATThe email format is invalid.
INVALID_DATE_FORMATThe date format is invalid. It should be in ISO 8601 date format.
INVALID_ENUM_VALUEThe enum value is invalid.
INVALID_SHIPPING_PREFERENCEThere's a shipping preference mismatch.
NO_SELECTION_MADENo shipping option is selected.
MULTIPLE_SELECTIONS_NOT_ALLOWEDMultiple shipping options are selected.
CURRENCY_CODE_INVALIDThere's a currency code inconsistency. For example, due to different currencies in the same request.

Validation example

import android.util.Log

val paypalConfig = PayPalComponentConfig(
    // ... configuration
    onSubmitError = { error ->
        Log.e("PayPal", "Validation error: $error")
        
        // Handle specific validation errors
        when {
            error.contains("payeeEmailAddress") -> {
                showError("Invalid email address format")
            }
            error.contains("currencyCode") -> {
                showError("Invalid currency code")
            }
            error.contains("REQUIRED_FIELD") -> {
                showError("Please fill in all required fields")
            }
            error.contains("MAX_LENGTH_EXCEEDED") -> {
                showError("Some fields exceed maximum length")
            }
            else -> {
                showError("Validation failed: $error")
            }
        }
        
        // Log for debugging
        crashlytics.log("PayPal validation error: $error")
    }
)

Custom validation

Pre-authorisation validation

Run a validation before the PayPal authorisation to catch issues early and prevent failed payments.

import kotlinx.coroutines.launch

val paypalConfig = PayPalComponentConfig(
    onSuccess = { data ->
        viewModelScope.launch {
            try {
                // 1. Business logic validation
                val orderValidation = validateOrder(
                    cartItems = getCartItems(),
                    customerLocation = getCustomerLocation(),
                    paymentAmount = getOrderTotal()
                )
                
                if (!orderValidation.isValid) {
                    throw ValidationException(orderValidation.reason)
                }
                
                // 2. Security validation
                val securityCheck = performSecurityValidation(
                    customerIP = getClientIP(),
                    deviceFingerprint = getDeviceFingerprint(),
                    paymentHistory = getCustomerPaymentHistory()
                )
                
                if (securityCheck.riskLevel == RiskLevel.HIGH) {
                    // Require additional verification
                    val verified = requestAdditionalVerification()
                    if (!verified) {
                        throw SecurityException("Additional verification required")
                    }
                }
                
                // 3. Inventory validation
                val inventoryCheck = validateInventory(getCartItems())
                if (!inventoryCheck.allAvailable) {
                    throw InventoryException("Some items are no longer available")
                }
                
                // 4. Regulatory compliance
                val complianceCheck = validateCompliance(
                    customerCountry = getCustomerCountry(),
                    orderAmount = getOrderTotal(),
                    productTypes = getProductTypes()
                )
                
                if (!complianceCheck.compliant) {
                    throw ComplianceException("Order does not meet regulatory requirements")
                }
                
                // If all validations pass, proceed with payment processing
                processPayPalPayment(data)
                
            } catch (e: Exception) {
                Log.e("PayPal", "Validation failed", e)
                showError(e.message ?: "Validation failed")
            }
        }
    }
)

Confirmation screen validation

Run validation on the confirmation screen before capturing funds, to ensure order integrity and prevent capture failures.

suspend fun confirmAndCaptureOrder(orderID: String) {
    try {
        // 1. Re-validate order (things might have changed)
        val currentOrderState = validateCurrentOrderState(orderID)
        if (!currentOrderState.isValid) {
            throw ValidationException("Order state has changed since authorisation")
        }
        
        // 2. Final inventory check
        val finalInventoryCheck = reserveInventory(orderID)
        if (!finalInventoryCheck.success) {
            throw InventoryException("Inventory no longer available")
        }
        
        // 3. Calculate final amounts (shipping, taxes, fees)
        val finalCalculation = calculateFinalAmounts(orderID)
        
        // 4. Validate amount changes are within acceptable limits
        val authData = getAuthorizationData(orderID)
        val amountDifference = Math.abs(finalCalculation.total - authData.authorizedAmount)
        val maxAllowedDifference = authData.authorizedAmount * 0.15 // 15% tolerance
        
        if (amountDifference > maxAllowedDifference) {
            throw ValidationException("Final amount differs too much from authorised amount")
        }
        
        // 5. Capture the payment
        captureAuthorizedPayment(orderID, finalCalculation.total)
        
    } catch (e: Exception) {
        handleCaptureValidationError(e)
    }
}

Validate geographic restrictions

Check in real-time if any cart items are restricted in the customer's country to prevent compliance violations.

data class GeographicValidationResult(
    val isValid: Boolean,
    val restrictedItems: List<CartItem>
)

fun validateGeographicRestrictions(
    customerCountry: String,
    cartItems: List<CartItem>
): GeographicValidationResult {
    val restrictedItems = cartItems.filter { item -> 
        item.restrictions?.countries?.contains(customerCountry) == true
    }
    
    return GeographicValidationResult(
        isValid = restrictedItems.isEmpty(),
        restrictedItems = restrictedItems
    )
}

// Usage in PayPal component
val paypalConfig = PayPalComponentConfig(
    onSuccess = { data ->
        viewModelScope.launch {
            val validation = validateGeographicRestrictions(
                customerCountry = getCustomerCountry(),
                cartItems = getCartItems()
            )
            
            if (!validation.isValid) {
                showError(
                    "The following items cannot be shipped to your country: " +
                    validation.restrictedItems.joinToString(", ") { it.name }
                )
                return@launch
            }
            
            processPayPalPayment(data)
        }
    }
)

Calculate a risk score

Calculate a comprehensive fraud risk score based on multiple behavioural and historical factors.

data class RiskFactors(
    val velocityScore: Double,
    val locationScore: Double,
    val deviceScore: Double,
    val historyScore: Double
)

data class FraudRiskResult(
    val score: Double,
    val riskLevel: RiskLevel,
    val factors: RiskFactors
)

enum class RiskLevel {
    LOW, MEDIUM, HIGH
}

suspend fun calculateFraudScore(transactionData: TransactionData): FraudRiskResult {
    val factors = RiskFactors(
        velocityScore = checkPaymentVelocity(transactionData.customerEmail),
        locationScore = validateLocation(transactionData.ipAddress),
        deviceScore = analyzeDeviceFingerprint(transactionData.deviceData),
        historyScore = analyzePaymentHistory(transactionData.customerId)
    )
    
    val totalScore = (factors.velocityScore + factors.locationScore + 
                     factors.deviceScore + factors.historyScore) / 4.0
    
    val riskLevel = when {
        totalScore > 0.8 -> RiskLevel.HIGH
        totalScore > 0.5 -> RiskLevel.MEDIUM
        else -> RiskLevel.LOW
    }
    
    return FraudRiskResult(
        score = totalScore,
        riskLevel = riskLevel,
        factors = factors
    )
}

// Usage in PayPal component
val paypalConfig = PayPalComponentConfig(
    onSuccess = { data ->
        viewModelScope.launch {
            val riskResult = calculateFraudScore(
                TransactionData(
                    customerEmail = getCurrentCustomerEmail(),
                    ipAddress = getClientIP(),
                    deviceData = getDeviceData(),
                    customerId = getCurrentCustomerId()
                )
            )
            
            when (riskResult.riskLevel) {
                RiskLevel.HIGH -> {
                    Log.w("PayPal", "High risk transaction detected")
                    // Require additional verification
                    if (!requestAdditionalVerification()) {
                        showError("Additional verification required")
                        return@launch
                    }
                }
                RiskLevel.MEDIUM -> {
                    Log.i("PayPal", "Medium risk transaction")
                    // Optional: Add extra monitoring
                    flagTransactionForReview(data)
                }
                RiskLevel.LOW -> {
                    Log.d("PayPal", "Low risk transaction")
                }
            }
            
            processPayPalPayment(data)
        }
    }
)

Shipping address validation

Validate shipping addresses in the onShippingAddressChange callback.

import org.json.JSONObject

val paypalConfig = PayPalComponentConfig(
    onShippingAddressChange = { data ->
        try {
            val jsonObject = JSONObject(data)
            val shippingAddress = jsonObject.optJSONObject("shippingAddress")
            
            if (shippingAddress != null) {
                val countryCode = shippingAddress.optString("countryCode", "")
                val postalCode = shippingAddress.optString("postalCode", "")
                val city = shippingAddress.optString("city", "")
                val state = shippingAddress.optString("state", "")
                
                // Validate country
                if (!isSupportedCountry(countryCode)) {
                    return@PayPalComponentConfig "reject:COUNTRY_NOT_SUPPORTED"
                }
                
                // Validate postal code format
                if (!isValidPostalCode(postalCode, countryCode)) {
                    return@PayPalComponentConfig "reject:INVALID_POSTAL_CODE"
                }
                
                // Check PO Box restrictions
                val addressLine1 = shippingAddress.optString("addressLine1", "")
                if (containsPOBox(addressLine1) && !allowPOBox()) {
                    return@PayPalComponentConfig "reject:PO_BOX_NOT_ALLOWED"
                }
                
                // Validate state for US addresses
                if (countryCode == "US" && !isValidUSState(state)) {
                    return@PayPalComponentConfig "reject:INVALID_STATE"
                }
                
                Log.d("PayPal", "Address validation passed")
                null // Allow the change
            } else {
                null
            }
        } catch (e: Exception) {
            Log.e("PayPal", "Address validation error", e)
            null // Allow on error
        }
    }
)

// Helper functions
fun isSupportedCountry(countryCode: String): Boolean {
    val supportedCountries = setOf("US", "GB", "CA", "AU", "DE", "FR", "IT", "ES")
    return countryCode in supportedCountries
}

fun isValidPostalCode(postalCode: String, countryCode: String): Boolean {
    return when (countryCode) {
        "US" -> postalCode.matches(Regex("^\\d{5}(-\\d{4})?$"))
        "GB" -> postalCode.matches(Regex("^[A-Z]{1,2}\\d[A-Z\\d]? ?\\d[A-Z]{2}$"))
        "CA" -> postalCode.matches(Regex("^[A-Z]\\d[A-Z] ?\\d[A-Z]\\d$"))
        else -> postalCode.isNotEmpty()
    }
}

fun containsPOBox(address: String): Boolean {
    val poBoxPatterns = listOf(
        "P\\.?O\\.? Box",
        "PO Box",
        "Post Office Box"
    )
    return poBoxPatterns.any { pattern ->
        address.contains(Regex(pattern, RegexOption.IGNORE_CASE))
    }
}

fun isValidUSState(state: String): Boolean {
    val validStates = setOf(
        "AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", "GA",
        "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", "MD",
        "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ",
        "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC",
        "SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV", "WI", "WY"
    )
    return state.uppercase() in validStates
}

Amount validation

Validate transaction amounts to prevent errors and fraud.

data class AmountValidationResult(
    val isValid: Boolean,
    val errors: List<String>
)

fun validateTransactionAmount(
    amount: Int,
    currency: String,
    customerType: String
): AmountValidationResult {
    val errors = mutableListOf<String>()
    
    // Minimum amount check
    val minimumAmount = when (currency) {
        "USD", "CAD", "AUD" -> 100 // $1.00
        "EUR", "GBP" -> 50  // €0.50 or £0.50
        else -> 100
    }
    
    if (amount < minimumAmount) {
        errors.add("Amount is below minimum of ${minimumAmount / 100.0} $currency")
    }
    
    // Maximum amount check
    val maximumAmount = when (customerType) {
        "new" -> 50000 // $500.00 for new customers
        "verified" -> 100000 // $1000.00 for verified
        "vip" -> Int.MAX_VALUE // No limit for VIP
        else -> 50000
    }
    
    if (amount > maximumAmount) {
        errors.add("Amount exceeds maximum of ${maximumAmount / 100.0} $currency for $customerType customers")
    }
    
    // Check for suspicious amounts (e.g., exactly $9999.99)
    if (amount == 999999) {
        errors.add("Suspicious transaction amount detected")
    }
    
    return AmountValidationResult(
        isValid = errors.isEmpty(),
        errors = errors
    )
}

// Usage
val paypalConfig = PayPalComponentConfig(
    onSuccess = { data ->
        viewModelScope.launch {
            val amountValidation = validateTransactionAmount(
                amount = transactionData.amount,
                currency = transactionData.currency.toString(),
                customerType = getCustomerType()
            )
            
            if (!amountValidation.isValid) {
                showError(amountValidation.errors.joinToString(", "))
                return@launch
            }
            
            processPayPalPayment(data)
        }
    }
)

Validation best practices

  1. Validate early: Perform validation before showing the PayPal button to prevent user frustration.

  2. Provide clear feedback: Show specific, actionable error messages to help users correct issues.

  3. Log validation errors: Track validation failures for debugging and improvement.

fun logValidationError(field: String, error: String, value: Any?) {
    val errorData = mapOf(
        "field" to field,
        "error" to error,
        "value" to value.toString(),
        "timestamp" to System.currentTimeMillis()
    )
    
    analytics.track("validation_error", errorData)
    crashlytics.log("Validation error: $field - $error")
}
  1. Handle edge cases: Consider unusual but valid inputs (e.g., international address formats).

  2. Test thoroughly: Test validation with various valid and invalid inputs across different devices and Android versions.