Learn about built-in validation and implement additional scenarios for iOS.
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 accessible via error.errorCode:
| Error code | Description |
|---|---|
VALIDATION_FAILED | PXP SDK validation errors. Check error.errorMessage for specific field details. |
PAYPAL_SUBMIT_ERROR | PayPal SDK submission errors during order creation. |
PAYPAL_ERROR | PayPal SDK runtime errors during payment processing. |
PAYPAL_CANCEL | User 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.
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)")
}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)
}
}
}
}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)
}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 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)
}
}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 creationValidate 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
}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
}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()
}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 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 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")
}