Learn about built-in validation and implement additional scenarios for Android.
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.
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.
The PayPal component returns structured error codes for different validation failures:
| Error code | Description |
|---|---|
REQUIRED_FIELD | At least one required field is missing. |
MAX_LENGTH_EXCEEDED | At least one field exceeds its maximum length. |
INVALID_LENGTH | At least one field has the incorrect length. |
INVALID_EMAIL_FORMAT | The email format is invalid. |
INVALID_DATE_FORMAT | The date format is invalid. It should be in ISO 8601 date format. |
INVALID_ENUM_VALUE | The enum value is invalid. |
INVALID_SHIPPING_PREFERENCE | There's a shipping preference mismatch. |
NO_SELECTION_MADE | No shipping option is selected. |
MULTIPLE_SELECTIONS_NOT_ALLOWED | Multiple shipping options are selected. |
CURRENCY_CODE_INVALID | There's a currency code inconsistency. For example, due to different currencies in the same request. |
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")
}
)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")
}
}
}
)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)
}
}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 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)
}
}
)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
}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)
}
}
)Validate early: Perform validation before showing the PayPal button to prevent user frustration.
Provide clear feedback: Show specific, actionable error messages to help users correct issues.
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")
}Handle edge cases: Consider unusual but valid inputs (e.g., international address formats).
Test thoroughly: Test validation with various valid and invalid inputs across different devices and Android versions.