Implement callbacks to customise your PayPal payout flow for iOS.
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.
The PayPal payout submission component supports the following events:
onClickonPrePayoutSubmitonPostPayoutonCancelonError
The following sections describe each callback in detail.
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.
This callback receives no parameters.
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()
}
)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.
This callback receives no parameters.
Returns an optional PrePayoutSubmitResult object:
| Property | Description |
|---|---|
isApprovedBool 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.
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)
}
)// 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?")
}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 | Description |
|---|---|
resultMerchantSubmitResult | The payout transaction result object. |
result.merchantTransactionIdString | Your unique transaction identifier. |
result.systemTransactionIdString | The PXP system transaction ID for tracking. |
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)
}
)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.
This callback receives no parameters.
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()
}
)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 | Description |
|---|---|
errorBaseSdkException | The error object containing details about what went wrong. |
error.errorCodeString | The error code for identifying specific error types. |
error.errorMessageString | A human-readable error message. |
error.localizedDescriptionString | The localised error description (same as errorMessage). |
| Error code | Description |
|---|---|
SDK0809 | PayoutPayerIdRequiredException: Payer ID is empty or not provided. |
SDK0810 | PayoutAmountInvalidException: Amount is NaN or infinite. |
SDK0811 | PayoutAmountNotPositiveException: Amount must be greater than zero. |
SDK0817 | PayoutPayerIdMaxLengthException: Payer ID exceeds 13 characters. |
SDK0818 | PayoutPayerIdInvalidException: Payer ID contains invalid characters. |
SDK0819 | PayoutFailedException: 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.
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
])
}
)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)")
}
}
}
}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()
}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)"
]
)
}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)
])
}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
}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.
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
])
}
)All analytics events inherit from BaseAnalyticsEvent and include the following properties:
| Property | Description |
|---|---|
eventNameString required | The name of the analytics event. |
sessionIdString required | The session ID associated with the event. |
timestampDate required | The timestamp when the event occurred. |
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.
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)")
}
)