Skip to content

Data validation

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

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 accessible via error.errorCode:

Error code Description
VALIDATION_FAILEDPXP SDK validation errors. Check error.errorMessage for specific field details.
PAYPAL_SUBMIT_ERRORPayPal SDK submission errors during order creation.
PAYPAL_ERRORPayPal SDK runtime errors during payment processing.
PAYPAL_CANCELUser cancelled the payment flow.

Note: Specific field validation details (required fields, format errors, length violations) are provided in error.errorMessage, not as separate error codes.

Validation example

let config = PayPalButtonComponentConfig()
// ... configuration

config.onSubmitError = { error in
    print("Validation error: \(error.errorMessage)")
    
    // Check error code
    switch error.errorCode {
    case "VALIDATION_FAILED":
        // Parse specific field errors from errorMessage
        let errorMessage = error.errorMessage
        
        if errorMessage.contains("payeeEmailAddress") {
            showError("Invalid email address format")
        } else if errorMessage.contains("currencyCode") {
            showError("Invalid currency code")
        } else if errorMessage.contains("is required") {
            showError("Please fill in all required fields")
        } else if errorMessage.contains("exceeds maximum length") {
            showError("Some fields exceed maximum length")
        } else {
            showError("Validation failed: \(errorMessage)")
        }
        
    case "PAYPAL_SUBMIT_ERROR":
        showError("Failed to create PayPal order. Please try again.")
        
    default:
        showError("An error occurred: \(error.errorMessage)")
    }
    
    // Log for debugging
    Crashlytics.crashlytics().log("PayPal error [\(error.errorCode)]: \(error.errorMessage)")
}

Custom validation

Pre-authorisation validation

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

let config = PayPalButtonComponentConfig()

config.onApprove = { approvalData in
    Task {
        do {
            // 1. Business logic validation
            let orderValidation = try await validateOrder(
                cartItems: getCartItems(),
                customerLocation: getCustomerLocation(),
                paymentAmount: getOrderTotal()
            )
            
            guard orderValidation.isValid else {
                throw NSError(domain: "ValidationError", code: 1001, 
                            userInfo: [NSLocalizedDescriptionKey: "Invalid order: \(orderValidation.reason)"])
            }
            
            // 2. Security validation
            let securityCheck = try await performSecurityValidation(
                customerIP: getClientIP(),
                deviceFingerprint: getDeviceFingerprint(),
                paymentHistory: getCustomerPaymentHistory()
            )
            
            if securityCheck.riskLevel == .high {
                // Require additional verification
                let verified = try await requestAdditionalVerification()
                guard verified else {
                    throw NSError(domain: "SecurityError", code: 2001,
                                userInfo: [NSLocalizedDescriptionKey: "Additional verification required"])
                }
            }
            
            // 3. Inventory validation
            let inventoryCheck = try await validateInventory(getCartItems())
            guard inventoryCheck.allAvailable else {
                throw NSError(domain: "InventoryError", code: 3001,
                            userInfo: [NSLocalizedDescriptionKey: "Some items are no longer available"])
            }
            
            // 4. Regulatory compliance
            let complianceCheck = try await validateCompliance(
                customerCountry: getCustomerCountry(),
                orderAmount: getOrderTotal(),
                productTypes: getProductTypes()
            )
            
            guard complianceCheck.compliant else {
                throw NSError(domain: "ComplianceError", code: 4001,
                            userInfo: [NSLocalizedDescriptionKey: "Order does not meet compliance requirements"])
            }
            
            // If all validations pass, proceed with payment processing
            try await processPayPalPayment(approvalData)
            
        } catch {
            print("Validation failed: \(error.localizedDescription)")
            await MainActor.run {
                showError(error.localizedDescription)
            }
        }
    }
}

Confirmation screen validation

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

func confirmAndCaptureOrder(orderID: String) async throws {
    // 1. Re-validate order (things might have changed)
    let currentOrderState = try await validateCurrentOrderState(orderID)
    guard currentOrderState.isValid else {
        throw NSError(domain: "ValidationError", code: 1002,
                    userInfo: [NSLocalizedDescriptionKey: "Order state has changed since authorization"])
    }
    
    // 2. Final inventory check
    let finalInventoryCheck = try await reserveInventory(orderID)
    guard finalInventoryCheck.success else {
        throw NSError(domain: "InventoryError", code: 3002,
                    userInfo: [NSLocalizedDescriptionKey: "Items are no longer available"])
    }
    
    // 3. Calculate final amounts (shipping, taxes, fees)
    let finalCalculation = try await calculateFinalAmounts(orderID)
    
    // 4. Validate amount changes are within acceptable limits
    let authData = try await getAuthorizationData(orderID)
    let amountDifference = abs(finalCalculation.total - authData.authorizedAmount)
    let maxAllowedDifference = authData.authorizedAmount * 0.15 // 15% tolerance
    
    guard amountDifference <= maxAllowedDifference else {
        throw NSError(domain: "ValidationError", code: 1003,
                    userInfo: [NSLocalizedDescriptionKey: "Amount difference exceeds acceptable tolerance"])
    }
    
    // 5. Capture the payment
    try await captureAuthorizedPayment(orderID, amount: finalCalculation.total)
}

Validate geographic restrictions

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

struct GeographicValidationResult {
    let isValid: Bool
    let restrictedItems: [CartItem]
}

func validateGeographicRestrictions(
    customerCountry: String,
    cartItems: [CartItem]
) -> GeographicValidationResult {
    let restrictedItems = cartItems.filter { item in
        item.restrictions?.countries.contains(customerCountry) == true
    }
    
    return GeographicValidationResult(
        isValid: restrictedItems.isEmpty,
        restrictedItems: restrictedItems
    )
}

// Usage in PayPal component
let config = PayPalButtonComponentConfig()
config.onApprove = { approvalData in
    Task {
        let validation = validateGeographicRestrictions(
            customerCountry: getCustomerCountry(),
            cartItems: getCartItems()
        )
        
        if !validation.isValid {
            let itemNames = validation.restrictedItems.map { $0.name }.joined(separator: ", ")
            await MainActor.run {
                showError("The following items cannot be shipped to your country: \(itemNames)")
            }
            return
        }
        
        try await processPayPalPayment(approvalData)
    }
}

Calculate a risk score

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

struct RiskFactors {
    let velocityScore: Double
    let locationScore: Double
    let deviceScore: Double
    let historyScore: Double
}

struct FraudRiskResult {
    let score: Double
    let riskLevel: RiskLevel
    let factors: RiskFactors
}

enum RiskLevel {
    case low, medium, high
}

func calculateFraudScore(transactionData: TransactionData) async throws -> FraudRiskResult {
    let factors = RiskFactors(
        velocityScore: try await checkPaymentVelocity(transactionData.customerEmail),
        locationScore: try await validateLocation(transactionData.ipAddress),
        deviceScore: try await analyzeDeviceFingerprint(transactionData.deviceData),
        historyScore: try await analyzePaymentHistory(transactionData.customerId)
    )
    
    let totalScore = (factors.velocityScore + factors.locationScore + 
                     factors.deviceScore + factors.historyScore) / 4.0
    
    let riskLevel: RiskLevel
    if totalScore > 0.8 {
        riskLevel = .high
    } else if totalScore > 0.5 {
        riskLevel = .medium
    } else {
        riskLevel = .low
    }
    
    return FraudRiskResult(
        score: totalScore,
        riskLevel: riskLevel,
        factors: factors
    )
}

// Usage in PayPal component
let config = PayPalButtonComponentConfig()
config.onApprove = { approvalData in
    Task {
        let riskResult = try await calculateFraudScore(
            TransactionData(
                customerEmail: getCurrentCustomerEmail(),
                ipAddress: getClientIP(),
                deviceData: getDeviceData(),
                customerId: getCurrentCustomerId()
            )
        )
        
        switch riskResult.riskLevel {
        case .high:
            print("High risk transaction detected")
            // Require additional verification
            let verified = try await requestAdditionalVerification()
            guard verified else {
                await MainActor.run {
                    showError("Additional verification required")
                }
                return
            }
        case .medium:
            print("Medium risk transaction")
            // Optional: Add extra monitoring
            await flagTransactionForReview(approvalData)
        case .low:
            print("Low risk transaction")
        }
        
        try await processPayPalPayment(approvalData)
    }
}

Validate payment amount limits

Ensure the payment amount meets minimum and maximum requirements.

struct AmountValidation {
    let isValid: Bool
    let error: String?
}

func validatePaymentAmount(
    amount: Double,
    currency: String,
    paymentMethod: String
) -> AmountValidation {
    // Define limits per payment method
    let limits: [String: (min: Double, max: Double)] = [
        "paypal": (1.0, 10000.0),
        "paylater": (30.0, 1500.0)
    ]
    
    guard let (minAmount, maxAmount) = limits[paymentMethod] else {
        return AmountValidation(isValid: false, error: "Unknown payment method")
    }
    
    // Currency symbols for display
    let currencySymbols: [String: String] = [
        "USD": "$",
        "EUR": "€",
        "GBP": "£"
    ]
    let symbol = currencySymbols[currency] ?? currency
    
    if amount < minAmount {
        return AmountValidation(
            isValid: false,
            error: "Minimum payment amount is \(symbol)\(minAmount)"
        )
    }
    
    if amount > maxAmount {
        return AmountValidation(
            isValid: false,
            error: "Maximum payment amount is \(symbol)\(maxAmount)"
        )
    }
    
    return AmountValidation(isValid: true, error: nil)
}

// Usage before creating component
let validation = validatePaymentAmount(
    amount: 100.0,
    currency: "USD",
    paymentMethod: "paypal"
)

if !validation.isValid {
    showError(validation.error ?? "Invalid amount")
    return
}

// Proceed with PayPal component creation

Email format validation

Validate email addresses before configuration.

func validateEmail(_ email: String) -> Bool {
    let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
    let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
    return emailPredicate.evaluate(with: email)
}

// Usage in configuration
let config = PayPalButtonComponentConfig()

if let payeeEmail = payeeEmailAddress {
    guard validateEmail(payeeEmail) else {
        showError("Invalid payee email address format")
        return
    }
    config.payeeEmailAddress = payeeEmail
}

Cart validation

Validate cart contents before initiating payment.

struct CartValidation {
    let isValid: Bool
    let errors: [String]
}

func validateCart(items: [CartItem]) -> CartValidation {
    var errors: [String] = []
    
    // Check cart is not empty
    if items.isEmpty {
        errors.append("Cart is empty")
    }
    
    // Validate each item
    for item in items {
        if item.quantity <= 0 {
            errors.append("\(item.name): Invalid quantity")
        }
        
        if item.price <= 0 {
            errors.append("\(item.name): Invalid price")
        }
        
        if item.name.isEmpty {
            errors.append("Item missing name")
        }
    }
    
    // Calculate total
    let total = items.reduce(0.0) { $0 + ($1.price * Double($1.quantity)) }
    if total <= 0 {
        errors.append("Cart total must be greater than zero")
    }
    
    return CartValidation(
        isValid: errors.isEmpty,
        errors: errors
    )
}

// Usage before payment
let cartValidation = validateCart(items: getCartItems())
if !cartValidation.isValid {
    showError("Cart validation failed:\n" + cartValidation.errors.joined(separator: "\n"))
    return
}

Best practices

Fail early

Validate data as early as possible in the payment flow to provide immediate feedback.

// Validate when user initiates checkout, not when PayPal button is clicked
@objc func checkoutButtonTapped() {
    // Validate cart
    let cartValidation = validateCart(items: cartItems)
    guard cartValidation.isValid else {
        showAlert(title: "Invalid Cart", message: cartValidation.errors.joined(separator: "\n"))
        return
    }
    
    // Validate amount
    let amountValidation = validatePaymentAmount(
        amount: getCartTotal(),
        currency: "USD",
        paymentMethod: "paypal"
    )
    guard amountValidation.isValid else {
        showAlert(title: "Invalid Amount", message: amountValidation.error ?? "Amount validation failed")
        return
    }
    
    // Show PayPal payment screen
    showPayPalScreen()
}

Provide clear error messages

When validation fails, provide specific, actionable error messages.

config.onSubmitError = { error in
    let errorMessage: String
    
    // Check error code and provide user-friendly messages
    switch error.errorCode {
    case "VALIDATION_FAILED":
        // Parse error message for specific issues
        let details = error.errorMessage
        if details.contains("payeeEmailAddress") {
            errorMessage = "Please enter a valid email address"
        } else if details.contains("amount") {
            errorMessage = "Payment amount must be between $1 and $10,000"
        } else if details.contains("is required") {
            errorMessage = "Please fill in all required fields"
        } else {
            errorMessage = "Validation failed: \(details)"
        }
        
    case "PAYPAL_SUBMIT_ERROR":
        errorMessage = "Failed to create order. Please try again."
        
    case "PAYPAL_ERROR":
        errorMessage = "Payment processing error: \(error.errorMessage)"
        
    default:
        errorMessage = "An error occurred: \(error.errorMessage)"
    }
    
    await MainActor.run {
        showAlert(title: "Payment Error", message: errorMessage)
    }
}

Log validation errors

Log all validation errors for debugging and analytics.

config.onSubmitError = { error in
    // Log to analytics
    Analytics.logEvent("paypal_validation_error", parameters: [
        "error_code": error.errorCode,
        "error_message": error.errorMessage,
        "timestamp": Date().timeIntervalSince1970
    ])
    
    // Log to crash reporting
    Crashlytics.crashlytics().record(error: error)
    
    // Log to console
    print("PayPal error [\(error.errorCode)]: \(error.errorMessage)")
}

Test edge cases

Test validation with edge cases and boundary values.

func testPaymentValidation() {
    // Test minimum amount
    assert(validatePaymentAmount(amount: 1.0, currency: "USD", paymentMethod: "paypal").isValid)
    assert(!validatePaymentAmount(amount: 0.99, currency: "USD", paymentMethod: "paypal").isValid)
    
    // Test maximum amount
    assert(validatePaymentAmount(amount: 10000.0, currency: "USD", paymentMethod: "paypal").isValid)
    assert(!validatePaymentAmount(amount: 10000.01, currency: "USD", paymentMethod: "paypal").isValid)
    
    // Test email validation
    assert(validateEmail("user@example.com"))
    assert(!validateEmail("invalid.email"))
    assert(!validateEmail("@example.com"))
    
    print("All validation tests passed")
}