Skip to content

Events

Implement callbacks to customise your PayPal payout flow for iOS.

Overview

The PayPal payout submission component emits events throughout the payout lifecycle. You can use these to implement callback functions, which allow you to inject your own business logic and user experience customisations into the payout flow at critical moments. They ensure that while the SDK handles the complex technical aspects of payout processing, you retain full control over the user experience and can seamlessly integrate payouts into your broader business workflows and systems.

The PayoutAmountComponent and PayPalPayoutReceiverComponent are display-only components and don't emit events.

Callbacks enable you to:

  • Validate business rules before payouts execute.
  • Implement custom approval flows for payout authorisation.
  • Display custom error, failure, or success messages.
  • Tailor user interfaces to match your brand's look and feel.
  • Integrate with your own systems for compliance or audit logging.
  • Control exactly how your recipients experience both successful and failed payouts.
  • Track user interactions and payout status for analytics.

All events are optional and can be mixed and matched based on your business needs.

Supported events

The PayPal payout submission component supports the following events:

  • onClick
  • onPrePayoutSubmit
  • onPostPayout
  • onCancel
  • onError

Callbacks

The following sections describe each callback in detail.

onClick

This callback is triggered when the user taps the payout button before payout validation begins.

You can use it to:

  • Trigger analytics tracking for user engagement.
  • Show loading indicators or status messages.
  • Perform pre-validation checks.
  • Log user interactions for monitoring.

Event data

This callback receives no parameters.

Example implementation

let config = PayoutSubmissionComponentConfig(
    submitText: "Withdraw to PayPal",
    onClick: {
        print("PayPal payout button clicked")
        
        // Track button click
        Analytics.track("paypal_payout_clicked", properties: [
            "timestamp": Date().timeIntervalSince1970,
            "payoutAmount": getCurrentPayoutAmount()
        ])
        
        // Show loading state
        showLoadingIndicator()
        
        // Perform pre-flight checks
        validateUserEligibleForPayout()
    }
)

onPrePayoutSubmit

This callback is triggered to allow merchant approval before the payout executes. This is where you implement your business logic to decide whether to proceed with the payout.

This callback is only triggered when proceedPayoutWithSdk: true in your PayPal configuration. If set to false, payouts are managed entirely by your backend.

You can use it to:

  • Show confirmation dialogs to verify payout details.
  • Implement compliance checks or fraud prevention.
  • Allow users to review payout amount and recipient.
  • Apply business rules before authorising payout.
  • Provide an opportunity to cancel the payout.

Event data

This callback receives no parameters.

Return value

Returns an optional PrePayoutSubmitResult object:

PropertyDescription
isApproved
Bool
required
Whether the merchant/user approves the payout. Return false to cancel.

The PayoutSubmissionComponent doesn't require a payerId because it uses the payer ID already configured in PayPalConfig.payout.paypalWallet.payerId for users with stored credentials.

Example implementation

let config = PayoutSubmissionComponentConfig(
    submitText: "Withdraw to PayPal",
    onPrePayoutSubmit: {
        print("Pre-payout approval requested")
        
        // Show approval dialog and wait for user response
        let approved = await showPayoutApprovalDialog(
            amount: getCurrentPayoutAmount(),
            currency: "USD",
            recipientEmail: getCurrentPayPalEmail()
        )
        
        // If user cancelled
        guard approved else {
            Analytics.track("payout_approval_rejected")
            return nil  // or return result with isApproved: false
        }
        
        // User approved - proceed with payout
        Analytics.track("payout_approval_granted", properties: [
            "amount": getCurrentPayoutAmount(),
            "currency": "USD",
            "timestamp": Date().timeIntervalSince1970
        ])
        
        // Return approval
        return PrePayoutSubmitResult(isApproved: true)
    }
)

Example with async confirmation dialog

// State for managing approval flow
@State private var showApprovalAlert = false
@State private var pendingApprovalContinuation: CheckedContinuation<PrePayoutSubmitResult?, Never>?

let config = PayoutSubmissionComponentConfig(
    submitText: "Withdraw to PayPal",
    onPrePayoutSubmit: { [self] in
        // Show approval dialog and wait for user response
        return await withCheckedContinuation { continuation in
            Task { @MainActor in
                self.pendingApprovalContinuation = continuation
                self.showApprovalAlert = true
            }
        }
    }
)

// In your SwiftUI view
.alert("Confirm Payout", isPresented: $showApprovalAlert) {
    Button("Cancel", role: .cancel) {
        // User rejected
        pendingApprovalContinuation?.resume(returning: nil)
        pendingApprovalContinuation = nil
    }
    Button("Confirm") {
        // User approved
        pendingApprovalContinuation?.resume(returning: 
            PrePayoutSubmitResult(isApproved: true)
        )
        pendingApprovalContinuation = nil
    }
} message: {
    Text("Send $\(payoutAmount) to your PayPal account?")
}

onPostPayout

This callback is triggered after the payout transaction completes successfully. This is where you update your system and notify the user of the successful payout.

You can use it to:

  • Update transaction records in your database.
  • Display success messages to users.
  • Send notifications about completed payouts.
  • Update user balance or pending payout status.
  • Navigate to a success screen.
  • Track successful payout metrics.

Event data

Event dataDescription
result
MerchantSubmitResult
The payout transaction result object.
result.merchantTransactionId
String
Your unique transaction identifier.
result.systemTransactionId
String
The PXP system transaction ID for tracking.

Example implementation

let config = PayoutSubmissionComponentConfig(
    submitText: "Withdraw to PayPal",
    onPostPayout: { result in
        print("Payout successful!")
        print("Merchant Transaction ID: \(result.merchantTransactionId)")
        print("System Transaction ID: \(result.systemTransactionId)")
        
        // Update payout record in your system
        updatePayoutStatus(
            merchantTxId: result.merchantTransactionId,
            systemTxId: result.systemTransactionId,
            status: "completed",
            completedAt: Date()
        )
        
        // Update user balance
        updateUserBalance(
            userId: currentUser.id,
            amount: getCurrentPayoutAmount(),
            type: "payout_completed"
        )
        
        // Send push notification
        sendPushNotification(
            title: "Payout Complete",
            body: "Your payout of $\(getCurrentPayoutAmount()) has been sent to your PayPal account"
        )
        
        // Track successful payout
        Analytics.track("payout_completed", properties: [
            "merchantTransactionId": result.merchantTransactionId,
            "systemTransactionId": result.systemTransactionId,
            "amount": getCurrentPayoutAmount(),
            "currency": "USD",
            "timestamp": Date().timeIntervalSince1970
        ])
        
        // Navigate to success screen
        navigateToPayoutSuccessScreen(result.merchantTransactionId)
    }
)

onCancel

onCancel

This callback is triggered when the user cancels the payout during the approval dialog (returning nil or isApproved: false from onPrePayoutSubmit).

You can use it to:

  • Track cancellation rates for conversion analysis.
  • Show helpful messages about the payout process.
  • Offer alternative payout methods.
  • Update UI to reflect cancelled state.
  • Log cancellation events for analytics.

Event data

This callback receives no parameters.

Example implementation

let config = PayoutSubmissionComponentConfig(
    submitText: "Withdraw to PayPal",
    onCancel: {
        print("User cancelled payout submission")
        
        // Track cancellation
        Analytics.track("payout_submission_cancelled", properties: [
            "amount": getCurrentPayoutAmount(),
            "timestamp": Date().timeIntervalSince1970
        ])
        
        // Hide loading indicator
        hideLoadingIndicator()
        
        // Show message
        showMessage("Withdrawal cancelled")
        
        // Reset UI state
        resetPayoutForm()
    }
)

onError

This callback is triggered when an error occurs during the payout process, such as network issues, invalid configuration, or payout processing errors.

You can use it to:

  • Log errors for debugging and monitoring.
  • Display user-friendly error messages.
  • Handle specific error types differently.
  • Implement retry logic for transient errors.
  • Offer alternative payout methods on failure.

Event data

Event dataDescription
error
BaseSdkException
The error object containing details about what went wrong.
error.errorCode
String
The error code for identifying specific error types.
error.errorMessage
String
A human-readable error message.
error.localizedDescription
String
The localised error description (same as errorMessage).

Common error codes

Error codeDescription
SDK0809PayoutPayerIdRequiredException: Payer ID is empty or not provided.
SDK0810PayoutAmountInvalidException: Amount is NaN or infinite.
SDK0811PayoutAmountNotPositiveException: Amount must be greater than zero.
SDK0817PayoutPayerIdMaxLengthException: Payer ID exceeds 13 characters.
SDK0818PayoutPayerIdInvalidException: Payer ID contains invalid characters.
SDK0819PayoutFailedException: Payout transaction failed.

For a complete list of error codes including SDK0812-SDK0816 (amount/currency validation) and SDK0803-SDK0808 (submission component errors), see Data validation.

Example implementation

let config = PayoutSubmissionComponentConfig(
    submitText: "Withdraw to PayPal",
    onError: { error in
        print("Payout error: \(error.errorMessage)")
        print("Error code: \(error.errorCode)")
        
        // Log error for debugging
        Crashlytics.crashlytics().log("PayPal payout error: \(error.errorMessage)")
        Crashlytics.crashlytics().record(error: error)
        
        // Hide loading indicator
        hideLoadingIndicator()
        
        // Handle different error types
        let errorMessage: String
        let errorCode = error.errorCode
        
        switch errorCode {
        case "SDK0809":
            errorMessage = "PayPal account information is missing. Please try again."
            
        case "SDK0810":
            errorMessage = "Invalid payout amount. Please contact support."
            
        case "SDK0811":
            errorMessage = "Payout amount must be greater than zero."
            
        case "SDK0817":
            errorMessage = "PayPal account identifier is too long. Please try again."
            
        case "SDK0818":
            errorMessage = "Invalid PayPal account format. Please try again."
            
        case "SDK0819":
            errorMessage = "Payout transaction failed. Please try again or contact support."
            
        default:
            errorMessage = "Payout failed. Please try again or contact support."
        }
        
        // Show error dialog
        showErrorDialog(
            title: "Payout Error",
            message: errorMessage
        )
        
        // Offer alternative actions based on error type
        if errorCode.contains("PayerId") {
            // Show contact support option
            showContactSupportOption()
        } else if errorCode.contains("Network") {
            // Offer retry
            showRetryOption()
        } else {
            // Show alternative payout methods
            showAlternativePayoutMethods()
        }
        
        // Track error metrics
        Analytics.track("payout_error", properties: [
            "errorCode": errorCode,
            "errorMessage": error.errorMessage,
            "payoutAmount": getCurrentPayoutAmount(),
            "timestamp": Date().timeIntervalSince1970
        ])
    }
)

Complete example

Here's a complete example with all event callbacks configured:

import SwiftUI
import PXPCheckoutSDK

struct PayoutView: View {
    @State private var pxpCheckout: PxpCheckout?
    @State private var submissionComponent: BaseComponent?
    @State private var showApprovalAlert = false
    @State private var pendingApprovalContinuation: CheckedContinuation<PrePayoutSubmitResult?, Never>?
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Receive Your Payout")
                .font(.headline)
            
            if let component = submissionComponent {
                component.buildContent()
                    .frame(height: 50)
            }
        }
        .padding()
        .alert("Confirm Payout", isPresented: $showApprovalAlert) {
            Button("Cancel", role: .cancel) {
                pendingApprovalContinuation?.resume(returning: nil)
                pendingApprovalContinuation = nil
            }
            Button("Confirm") {
                pendingApprovalContinuation?.resume(returning: 
                    PrePayoutSubmitResult(isApproved: true)
                )
                pendingApprovalContinuation = nil
            }
        } message: {
            Text("Send $100.00 to your PayPal account?")
        }
        .onAppear {
            createPayoutComponent()
        }
    }
    
    private func createPayoutComponent() {
        Task {
            do {
                // Initialise SDK (see Setup for full configuration)
                let checkout = try await initializePxpCheckout()
                
                // Configure payout component with all callbacks
                let config = PayoutSubmissionComponentConfig(
                    submitText: "Withdraw to PayPal",
                    submitAccessibilityLabel: "Withdraw funds to PayPal account",
                    
                    // 1. Button clicked
                    onClick: {
                        print("📱 PayPal payout button clicked")
                        Analytics.track("paypal_payout_clicked")
                    },
                    
                    // 2. Pre-payout approval
                    onPrePayoutSubmit: { [self] in
                        print("💰 Pre-payout approval requested")
                        
                        // Show approval dialog and wait for user response
                        return await withCheckedContinuation { continuation in
                            Task { @MainActor in
                                self.pendingApprovalContinuation = continuation
                                self.showApprovalAlert = true
                            }
                        }
                    },
                    
                    // 3. Payout completed
                    onPostPayout: { result in
                        print("🎉 Payout successful!")
                        print("   Merchant TX ID: \(result.merchantTransactionId)")
                        print("   System TX ID: \(result.systemTransactionId)")
                        
                        // Update payout status
                        updatePayoutStatus(
                            merchantTxId: result.merchantTransactionId,
                            status: "completed"
                        )
                        
                        // Show success message
                        showSuccessMessage("Payout completed!")
                        
                        // Track success
                        Analytics.track("payout_completed", properties: [
                            "merchantTransactionId": result.merchantTransactionId,
                            "systemTransactionId": result.systemTransactionId
                        ])
                    },
                    
                    // 4. User cancelled
                    onCancel: {
                        print("❌ User cancelled payout")
                        
                        // Track cancellation
                        Analytics.track("payout_cancelled")
                        
                        // Show message
                        showMessage("Withdrawal cancelled")
                    },
                    
                    // 5. Error occurred
                    onError: { error in
                        print("⚠️ Error occurred:")
                        print("   Code: \(error.errorCode)")
                        print("   Message: \(error.errorMessage)")
                        
                        // Log error
                        logError(error)
                        
                        // Show error dialog
                        showErrorDialog(
                            title: "Payout Error",
                            message: error.errorMessage
                        )
                        
                        // Track error
                        Analytics.track("payout_error", properties: [
                            "errorCode": error.errorCode,
                            "errorMessage": error.errorMessage
                        ])
                    }
                )
                
                // Create component
                let component = try checkout.create(
                    .payoutSubmission,
                    componentConfig: config
                )
                
                await MainActor.run {
                    self.submissionComponent = component
                }
            } catch {
                print("Failed to create payout component: \(error)")
            }
        }
    }
}

Best practices

Error handling

Always implement the onError callback to handle failures gracefully:

config.onError = { error in
    // Log for debugging
    logError(error)
    
    // Show user-friendly message
    showErrorDialog(getUserFriendlyMessage(for: error.errorCode))
    
    // Track for analytics
    trackError(error)
    
    // Offer alternatives
    showAlternativeOptions()
}

Approval flow

For onPrePayoutSubmit, always show clear information to users:

config.onPrePayoutSubmit = {
    return await showApprovalDialog(
        title: "Confirm Payout",
        message: "Send $\(amount) to your PayPal account?",
        details: [
            "Amount: $\(amount)",
            "Fee: $\(fee)",
            "Total: $\(total)",
            "PayPal Account: \(email)"
        ]
    )
}

Analytics tracking

Track key events for monitoring and optimisation:

// Track funnel
onClick: {
    Analytics.track("payout_started")
}

onPostPayout: { _ in
    Analytics.track("payout_completed")
}

// Track drop-offs
onCancel: {
    Analytics.track("payout_cancelled")
}

onError: { error in
    Analytics.track("payout_error", properties: [
        "errorCode": error.errorCode,
        "stage": determineErrorStage(error)
    ])
}

State management

Maintain clean state throughout the payout flow:

enum PayoutState {
    case idle
    case awaitingApproval
    case processing
    case completed
    case failed
    case cancelled
}

@State private var payoutState: PayoutState = .idle

// Update state in callbacks
onClick: {
    payoutState = .awaitingApproval
}

onPrePayoutSubmit: {
    payoutState = .processing
    // ...
}

onPostPayout: { _ in
    payoutState = .completed
}

SDK analytics events

In addition to component-level callbacks, you can receive analytics events from the SDK by configuring the analyticsEvent callback on CheckoutConfig. This provides visibility into SDK-level events for monitoring, debugging, and analytics integration.

Configuration

Configure the analytics callback when initialising the SDK:

let checkoutConfig = CheckoutConfig(
    environment: .test,
    session: sessionData,
    transactionData: transactionData,
    merchantShopperId: "user-123",
    ownerId: "merchant-456",
    paypalConfig: paypalConfig,
    analyticsEvent: { event in
        // Handle analytics events
        print("Analytics event: \(event.eventName)")
        
        // Forward to your analytics service
        Analytics.track(event.eventName, properties: [
            "timestamp": Date().timeIntervalSince1970
        ])
    }
)

BaseAnalyticsEvent

All analytics events inherit from BaseAnalyticsEvent and include the following properties:

PropertyDescription
eventName
String
required
The name of the analytics event.
sessionId
String
required
The session ID associated with the event.
timestamp
Date
required
The timestamp when the event occurred.

Use cases

Use the analytics callback to:

  • Monitor SDK performance: Track initialisation times, component lifecycle events, and error rates.
  • Debug integration issues: Log events during development to understand the SDK flow.
  • Integration with analytics platforms: Forward events to services like Firebase Analytics, Amplitude, or Mixpanel.
  • Audit logging: Record SDK events for compliance and audit purposes.

Example: Comprehensive analytics tracking

let checkoutConfig = CheckoutConfig(
    environment: .test,
    session: sessionData,
    transactionData: transactionData,
    merchantShopperId: "user-123",
    ownerId: "merchant-456",
    paypalConfig: paypalConfig,
    analyticsEvent: { event in
        // Log to console during development
        #if DEBUG
        print("📊 SDK Analytics: \(event.eventName)")
        #endif
        
        // Send to analytics service
        AnalyticsService.shared.track(
            event: event.eventName,
            properties: [
                "sdk_version": "1.0.0",
                "platform": "iOS",
                "environment": "test"
            ]
        )
        
        // Log to crash reporting for debugging
        Crashlytics.crashlytics().log("SDK event: \(event.eventName)")
    }
)