Skip to content

Submission component

Learn how to configure the payout submission component for iOS.

Overview

The PayoutSubmissionComponent provides a submit button to execute payouts for returning users who have already linked their PayPal accounts. This component handles payout validation, merchant approval, and transaction execution. Use this component alongside the PayoutAmountComponent and PayPalPayoutReceiverComponent to create a complete direct payout experience.

Basic usage

Minimal configuration

The payout submission component requires minimal component-level configuration, but depends on PayPal wallet credentials in CheckoutConfig:

// Required: Configure PayPal wallet in CheckoutConfig
let paypalConfig = PayPalConfig(
    payout: PayPalPayoutConfig(
        paypalWallet: PayPalWallet(
            email: "user@example.com",
            payerId: "PAYERID123ABC",
            proceedPayoutWithSdk: true
        )
    )
)

let checkoutConfig = CheckoutConfig(
    environment: .test,
    session: sessionData,
    transactionData: transactionData,
    merchantShopperId: "user-123",
    ownerId: "merchant-456",
    paypalConfig: paypalConfig
)

let pxpCheckout = try PxpCheckout.initialize(config: checkoutConfig)

// Then create the component
let config = PayoutSubmissionComponentConfig()
let component = try pxpCheckout.create(.payoutSubmission, componentConfig: config)

The paypalConfig.payout.paypalWallet.email and paypalConfig.payout.paypalWallet.payerId fields are required in CheckoutConfig before create(.payoutSubmission, ...) can succeed. These credentials should be stored from a previous PayPal OAuth flow.

With button text

Customise the button text:

let config = PayoutSubmissionComponentConfig(
    submitText: "Withdraw to PayPal",
    submitAccessibilityLabel: "Withdraw funds to PayPal account"
)
let component = try pxpCheckout.create(.payoutSubmission, componentConfig: config)
PropertyDescription
submitText
String?
The custom text for the submit button. Defaults to "Withdraw with PayPal".
submitAccessibilityLabel
String?
The accessibility label for the button. Defaults to "Withdraw with PayPal".

With event handlers

Add callbacks to handle the payout flow:

let config = PayoutSubmissionComponentConfig(
    submitText: "Withdraw Now",
    
    onClick: {
        print("Payout button clicked")
    },
    
    onPrePayoutSubmit: {
        let approved = await showApprovalDialog()
        guard approved else { return nil }
        return PrePayoutSubmitResult(isApproved: true)
    },
    
    onPostPayout: { result in
        print("Payout completed: \(result.merchantTransactionId)")
    },
    
    onError: { error in
        if let sdkError = error as? BaseSdkException {
            print("Error: \(sdkError.errorMessage)")
        }
    }
)
PropertyDescription
onClick
(() -> Void)?
Called when the button is clicked, before payout execution.
onPrePayoutSubmit
(() async -> PrePayoutSubmitResult?)?
Called for merchant approval before payout execution. Only used when proceedPayoutWithSdk is true. Return nil to cancel.
onPostPayout
((MerchantSubmitResult) -> Void)?
Called after the payout completes successfully.
onError
((Error) -> Void)?
Called when an error occurs. Cast to BaseSdkException to access error details.

Advanced configuration

For more complex implementations, configure styling and callbacks:

let config = PayoutSubmissionComponentConfig(
    recipientType: .email,
    wallet: .paypal,
    submitText: "Confirm Withdrawal",
    submitAccessibilityLabel: "Confirm withdrawal to PayPal",
    
    // Custom button styling
    styles: ButtonStateStyles(
        base: FieldStyle(
            color: .white,
            fontSize: 16,
            backgroundColor: Color(red: 0.0, green: 0.44, blue: 0.73),
            borderColor: Color(red: 0.0, green: 0.44, blue: 0.73),
            borderWidth: 0,
            cornerRadius: 8,
            padding: EdgeInsets(top: 14, leading: 24, bottom: 14, trailing: 24),
            opacity: 1.0
        ),
        disabled: FieldStyle(
            color: Color(.systemGray),
            backgroundColor: Color(.systemGray5),
            opacity: 0.6
        ),
        loading: FieldStyle(
            color: .white,
            backgroundColor: Color(red: 0.0, green: 0.37, blue: 0.65),
            opacity: 0.8
        )
    ),
    
    onClick: { },
    onPrePayoutSubmit: {
        // Perform validation and approval logic
        return PrePayoutSubmitResult(isApproved: true)
    },
    onPostPayout: { result in },
    onError: { error in }
)
PropertyDescription
recipientType
PayoutRecipientType
The recipient identifier type used in the SDK configuration. Note: The current SDK implementation validates .email in the PayPal wallet config, but internally uses the payerId for payout execution. Defaults to .email.
wallet
PayPalPayoutWalletType
The wallet type. Currently only .paypal is supported. Any other value throws PayoutWalletInvalidException. Defaults to .paypal.
submitText
String?
The custom text for the submit button.
submitAccessibilityLabel
String?
The accessibility label for the button.
styles
ButtonStateStyles?
Custom button styles for different states.

Event handling

The payout submission component provides event handlers to manage the payout flow:

let config = PayoutSubmissionComponentConfig(
    submitText: "Withdraw",
    onClick: { },
    onPrePayoutSubmit: {
        return PrePayoutSubmitResult(isApproved: true)
    },
    onPostPayout: { result in },
    onError: { error in }
)
CallbackDescription
onClick: (() -> Void)?Called when the button is clicked, before payout execution begins.
onPrePayoutSubmit: (() async -> PrePayoutSubmitResult?)?Called for merchant approval before payout execution. Required for payout execution when proceedPayoutWithSdk is true. Return nil or isApproved: false to prevent payout execution.
onPostPayout: ((MerchantSubmitResult) -> Void)?Called when payout completes successfully. Receives transaction IDs.
onError: ((BaseSdkException) -> Void)?Called when an error occurs during payout execution. Note: Most validation errors occur during component creation, not during button click.

Payout execution only happens when all three conditions are met:

  • paypalConfig.payout.paypalWallet.proceedPayoutWithSdk is set to true.
  • onPrePayoutSubmit callback is provided.
  • onPrePayoutSubmit returns PrePayoutSubmitResult(isApproved: true).

If any condition isn't met, the payout won't execute.

The onCancel callback is currently not invoked by the SDK implementation.

onClick

Triggered when the user taps the submit button:

config.onClick = {
    print("Payout button clicked")
    
    // Track analytics
    Analytics.track("payout_submit_clicked", properties: [
        "userId": currentUser.id,
        "amount": getCurrentPayoutAmount()
    ])
}

onPrePayoutSubmit

Triggered before payout execution when proceedPayoutWithSdk is true, allowing you to implement approval flows:

config.onPrePayoutSubmit = {
    // Perform custom validation
    let isValid = await validatePayoutEligibility()
    guard isValid else {
        showError("You are not eligible for this payout")
        return nil
    }
    
    // Show approval dialog
    let approved = await showApprovalDialog(
        amount: getCurrentPayoutAmount(),
        recipient: getStoredPayPalEmail()
    )
    
    guard approved else {
        Analytics.track("payout_approval_rejected")
        return nil
    }
    
    Analytics.track("payout_approval_granted")
    return PrePayoutSubmitResult(isApproved: true)
}

Payout execution only happens when all three conditions are met:

  • paypalConfig.payout.paypalWallet.proceedPayoutWithSdk is set to true.
  • This onPrePayoutSubmit callback is provided.
  • This callback returns PrePayoutSubmitResult(isApproved: true).

If this callback isn't provided, returns nil, or returns isApproved: false, the payout won't execute.

Return value

PropertyDescription
isApprovedBool
required
Whether the payout is approved.

onPostPayout

Triggered after the payout transaction completes successfully:

config.onPostPayout = { result in
    print("Payout completed!")
    print("Merchant TX ID: \(result.merchantTransactionId)")
    print("System TX ID: \(result.systemTransactionId)")
    
    // Update records
    updatePayoutRecord(
        merchantTxId: result.merchantTransactionId,
        systemTxId: result.systemTransactionId,
        status: "completed"
    )
    
    // Update user balance
    updateUserBalance(deduct: getCurrentPayoutAmount())
    
    // Show success message
    showSuccessMessage("Your payout has been sent!")
    
    // Track analytics
    Analytics.track("payout_completed", properties: [
        "transactionId": result.merchantTransactionId,
        "amount": getCurrentPayoutAmount()
    ])
}

Event data

PropertyDescription
result.merchantTransactionIdString
required
Your unique transaction identifier.
result.systemTransactionIdString
required
The PXP system transaction ID.

onError

Triggered when an error occurs during payout execution:

Most payout validation errors (receiver missing, invalid wallet configuration, etc.) are thrown during component creation via pxpCheckout.create(.payoutSubmission, ...) and must be caught with try-catch. The onError callback is primarily for errors that occur during the actual payout transaction execution.

config.onError = { error in
    guard let sdkError = error as? BaseSdkException else {
        print("Unknown error: \(error.localizedDescription)")
        return
    }
    
    print("Payout error: \(sdkError.errorMessage)")
    print("Error code: \(sdkError.errorCode)")
    
    // Handle specific errors
    let errorMessage: String
    
    switch sdkError.errorCode {
    case "SDK0816":
        errorMessage = "PayPal email address is too long."
    case "SDK0817", "SDK0818":
        errorMessage = "Invalid payer ID. Please re-link your PayPal account."
    case "SDK0819":
        errorMessage = "Payout transaction failed. Please try again."
    default:
        // Backend/API/HTTP error codes may also appear here
        errorMessage = "Payout failed. Please try again."
    }
    
    Task { @MainActor in
        showErrorDialog(title: "Payout Error", message: errorMessage)
    }
    
    // Track error
    Analytics.track("payout_error", properties: [
        "errorCode": sdkError.errorCode,
        "errorMessage": sdkError.errorMessage
    ])
}

Styling

Default styling

The payout submission component renders with default button styles:

// Base state (default)
FieldStyle(
    color: Color(red: 9/255, green: 9/255, blue: 12/255),
    fontSize: 16,
    backgroundColor: Color(red: 1.0, green: 0.765, blue: 0.220),  // PayPal gold
    borderColor: .clear,
    borderWidth: 0,
    cornerRadius: 3,
    padding: EdgeInsets(top: 16, leading: 12, bottom: 16, trailing: 12),
    opacity: 1.0,
    icon: IconStyle(size: CGSize(width: 60, height: 16))
)

Custom styling

Override the default appearance with custom styles for three button states:

let config = PayoutSubmissionComponentConfig(
    submitText: "Withdraw Now",
    styles: ButtonStateStyles(
        base: FieldStyle(
            color: .white,
            fontSize: 18,
            backgroundColor: Color(red: 0.2, green: 0.7, blue: 0.3),  // Green
            borderColor: Color(red: 0.2, green: 0.7, blue: 0.3),
            borderWidth: 0,
            cornerRadius: 12,
            padding: EdgeInsets(top: 16, leading: 32, bottom: 16, trailing: 32),
            opacity: 1.0
        ),
        disabled: FieldStyle(
            color: Color(.systemGray2),
            backgroundColor: Color(.systemGray5),
            borderColor: Color(.systemGray5),
            opacity: 0.5
        ),
        loading: FieldStyle(
            color: .white,
            backgroundColor: Color(red: 0.15, green: 0.55, blue: 0.25),  // Darker green
            opacity: 0.9
        )
    )
)
Style StateDescription
baseThe normal interactive state. Applied when the button is ready to be clicked.
disabledThe disabled state. Can be set by calling setDisabled(_:) on the component.
loadingThe loading state. Applied during payout processing.

Brand-specific styling

Apply your brand colours:

struct BrandColors {
    static let primary = Color(red: 0.17, green: 0.24, blue: 0.31)
    static let accent = Color(red: 0.20, green: 0.60, blue: 0.86)
    static let success = Color(red: 0.18, green: 0.80, blue: 0.44)
}

let config = PayoutSubmissionComponentConfig(
    submitText: "Complete Withdrawal",
    styles: ButtonStateStyles(
        base: FieldStyle(
            color: .white,
            fontSize: 16,
            backgroundColor: BrandColors.success,
            borderColor: BrandColors.success,
            borderWidth: 0,
            cornerRadius: 8,
            padding: EdgeInsets(top: 14, leading: 28, bottom: 14, trailing: 28)
        ),
        disabled: FieldStyle(
            backgroundColor: Color(.systemGray4),
            opacity: 0.6
        ),
        loading: FieldStyle(
            backgroundColor: BrandColors.primary,
            opacity: 0.9
        )
    )
)

FieldStyle properties

The submission button applies the following FieldStyle properties:

PropertyTypeDescriptionExample
colorColor?The text/foreground colour..white
fontFont?The SwiftUI font for the text..body
fontSizeCGFloat?The font size in points.16
backgroundColorColor?The background colour.Color(red: 0.0, green: 0.44, blue: 0.73)
borderColorColor?The border colour..blue
borderWidthCGFloat?The border width in points.1
cornerRadiusCGFloat?The corner radius in points.8
paddingEdgeInsets?The padding around the content.EdgeInsets(top: 14, leading: 24, bottom: 14, trailing: 24)
opacityDouble?The opacity (0.0 - 1.0).0.8
iconIconStyle?Icon styling with size and tint colour.IconStyle(size: CGSize(width: 24, height: 24), tintColor: .white)

Methods

buildContent()

Renders the submit button in SwiftUI:

struct PayoutView: View {
    @State private var submissionComponent: BaseComponent?
    
    var body: some View {
        VStack {
            if let component = submissionComponent {
                component.buildContent()
                    .frame(maxWidth: .infinity)
                    .frame(height: 50)
            }
        }
        .onAppear {
            createSubmissionComponent()
        }
    }
    
    private func createSubmissionComponent() {
        Task {
            let config = PayoutSubmissionComponentConfig(
                submitText: "Withdraw to PayPal",
                onPostPayout: { result in
                    print("Success: \(result.merchantTransactionId)")
                }
            )
            
            let component = try pxpCheckout.create(.payoutSubmission, componentConfig: config)
            
            await MainActor.run {
                self.submissionComponent = component
            }
        }
    }
}

Examples

Basic submission

A simple implementation with essential callbacks:

import SwiftUI
import PXPCheckoutSDK

struct BasicSubmissionView: View {
    @State private var submissionComponent: BaseComponent?
    @State private var payoutStatus: String?
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Withdraw Funds")
                .font(.title)
            
            if let component = submissionComponent {
                component.buildContent()
                    .frame(maxWidth: .infinity)
                    .frame(height: 50)
            }
            
            if let status = payoutStatus {
                Text(status)
                    .foregroundColor(.secondary)
            }
        }
        .padding()
        .onAppear {
            createComponent()
        }
    }
    
    private func createComponent() {
        Task {
            let config = PayoutSubmissionComponentConfig(
                submitText: "Withdraw to PayPal",
                
                onClick: {
                    print("Button clicked")
                },
                
                onPostPayout: { result in
                    Task { @MainActor in
                        payoutStatus = "Payout completed! TX: \(result.merchantTransactionId)"
                    }
                },
                
                onError: { error in
                    Task { @MainActor in
                        if let sdkError = error as? BaseSdkException {
                            payoutStatus = "Error: \(sdkError.errorMessage)"
                        } else {
                            payoutStatus = "Error: \(error.localizedDescription)"
                        }
                    }
                }
            )
            
            let component = try pxpCheckout.create(.payoutSubmission, componentConfig: config)
            
            await MainActor.run {
                self.submissionComponent = component
            }
        }
    }
}

With approval dialog

Implementation with SwiftUI alert for approval:

struct ApprovalPayoutView: View {
    @State private var submissionComponent: BaseComponent?
    @State private var showApprovalAlert = false
    @State private var pendingApprovalContinuation: CheckedContinuation<PrePayoutSubmitResult?, Never>?
    
    let payoutAmount: Decimal = 100.00
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Withdraw $\(String(format: "%.2f", payoutAmount))")
                .font(.title)
            
            if let component = submissionComponent {
                component.buildContent()
                    .frame(maxWidth: .infinity)
                    .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 $\(String(format: "%.2f", payoutAmount)) to your PayPal account?")
        }
        .onAppear {
            createComponent()
        }
    }
    
    private func createComponent() {
        Task {
            let config = PayoutSubmissionComponentConfig(
                submitText: "Withdraw Now",
                
                onPrePayoutSubmit: { [self] in
                    return await withCheckedContinuation { continuation in
                        Task { @MainActor in
                            self.pendingApprovalContinuation = continuation
                            self.showApprovalAlert = true
                        }
                    }
                },
                
                onPostPayout: { result in
                    print("Payout completed: \(result.merchantTransactionId)")
                    showSuccessScreen()
                },
                
                onError: { error in
                    if let sdkError = error as? BaseSdkException {
                        showErrorDialog(sdkError.errorMessage)
                    } else {
                        showErrorDialog(error.localizedDescription)
                    }
                }
            )
            
            let component = try pxpCheckout.create(.payoutSubmission, componentConfig: config)
            
            await MainActor.run {
                self.submissionComponent = component
            }
        }
    }
}

Complete direct payout flow

Full implementation with all three direct payout components:

import SwiftUI
import PXPCheckoutSDK

struct DirectPayoutView: View {
    @State private var pxpCheckout: PxpCheckout?
    @State private var amountComponent: BaseComponent?
    @State private var receiverComponent: BaseComponent?
    @State private var submissionComponent: BaseComponent?
    @State private var isLoading = true
    @State private var showApprovalAlert = false
    @State private var showSuccessAlert = false
    @State private var pendingApprovalContinuation: CheckedContinuation<PrePayoutSubmitResult?, Never>?
    @State private var completedTransactionId: String?
    
    let payoutAmount: Decimal = 100.00
    let storedPayPalEmail: String = "user@example.com"
    let storedPayerId: String = "PAYERID123ABC"
    
    var body: some View {
        NavigationView {
            VStack(spacing: 24) {
                if isLoading {
                    ProgressView("Loading...")
                } else {
                    // Header
                    VStack(spacing: 8) {
                        Text("Withdraw to PayPal")
                            .font(.title)
                            .fontWeight(.bold)
                        
                        Text("Funds will arrive in 1-2 business days")
                            .font(.subheadline)
                            .foregroundColor(.secondary)
                    }
                    
                    // Amount display
                    if let amount = amountComponent {
                        amount.buildContent()
                            .frame(maxWidth: .infinity)
                    }
                    
                    // Receiver display
                    if let receiver = receiverComponent {
                        receiver.buildContent()
                            .frame(maxWidth: .infinity)
                    }
                    
                    Spacer()
                    
                    // Submit button
                    if let submit = submissionComponent {
                        submit.buildContent()
                            .frame(maxWidth: .infinity)
                            .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 $\(String(format: "%.2f", payoutAmount)) to \(storedPayPalEmail)?")
            }
            .alert("Payout Successful!", isPresented: $showSuccessAlert) {
                Button("Done") {
                    // Navigate away
                }
            } message: {
                Text("Your payout has been sent. Transaction ID: \(completedTransactionId ?? "")")
            }
            .onAppear {
                initializePayoutComponents()
            }
        }
    }
    
    private func initializePayoutComponents() {
        Task {
            do {
                // Initialise SDK with stored credentials
                let sessionData = SessionData(
                    sessionId: "your-session-id",
                    hmacKey: "your-hmac-key",
                    encryptionKey: "your-encryption-key"
                )
                
                let transactionData = TransactionData(
                    amount: payoutAmount,
                    currency: "USD",
                    entryType: .ecom,
                    intent: TransactionIntentData(card: nil, paypal: .payout),
                    merchantTransactionId: "payout-\(UUID().uuidString)",
                    merchantTransactionDate: { Date() }
                )
                
                let paypalConfig = PayPalConfig(
                    payout: PayPalPayoutConfig(
                        paypalWallet: PayPalWallet(
                            email: storedPayPalEmail,
                            payerId: storedPayerId,
                            proceedPayoutWithSdk: true
                        )
                    )
                )
                
                let checkoutConfig = CheckoutConfig(
                    environment: .test,
                    session: sessionData,
                    transactionData: transactionData,
                    merchantShopperId: "user-123",
                    ownerId: "merchant-456",
                    paypalConfig: paypalConfig
                )
                
                let pxpCheckout = try PxpCheckout.initialize(config: checkoutConfig)
                self.pxpCheckout = pxpCheckout
                
                // Create amount component
                let amountConfig = PayoutAmountComponentConfig(
                    label: "Payout Amount",
                    styles: FieldStyle(
                        backgroundColor: Color(.systemGray6),
                        cornerRadius: 12,
                        padding: EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20),
                        textAlignment: .center
                    ),
                    inputStyles: FieldInputStateStyles(
                        base: FieldInputStyle(
                            color: Color(red: 0.2, green: 0.7, blue: 0.3),
                            fontSize: 32,
                            fontWeight: .bold
                        )
                    )
                )
                let amount = try pxpCheckout.create(.payoutAmount, componentConfig: amountConfig)
                
                // Create receiver component
                let receiverConfig = PayPalPayoutReceiverComponentConfig(
                    label: "PayPal Account",
                    applyMask: true,
                    showMaskToggle: true,
                    styles: FieldStyle(
                        backgroundColor: Color(red: 0.0, green: 0.44, blue: 0.73).opacity(0.05),
                        borderColor: Color(red: 0.0, green: 0.44, blue: 0.73).opacity(0.3),
                        borderWidth: 1,
                        cornerRadius: 12,
                        padding: EdgeInsets(top: 16, leading: 20, bottom: 16, trailing: 20)
                    )
                )
                let receiver = try pxpCheckout.create(.paypalPayoutReceiver, componentConfig: receiverConfig)
                
                // Create submission component
                let submissionConfig = PayoutSubmissionComponentConfig(
                    submitText: "Withdraw to PayPal",
                    submitAccessibilityLabel: "Withdraw funds to PayPal account",
                    
                    styles: ButtonStateStyles(
                        base: FieldStyle(
                            color: .white,
                            fontSize: 18,
                            backgroundColor: Color(red: 0.0, green: 0.44, blue: 0.73),
                            cornerRadius: 12,
                            padding: EdgeInsets(top: 16, leading: 32, bottom: 16, trailing: 32)
                        ),
                        disabled: FieldStyle(
                            backgroundColor: Color(.systemGray4),
                            opacity: 0.6
                        ),
                        loading: FieldStyle(
                            backgroundColor: Color(red: 0.0, green: 0.37, blue: 0.65),
                            opacity: 0.9
                        )
                    ),
                    
                    onClick: {
                        print("Withdraw button clicked")
                        Analytics.track("direct_payout_clicked")
                    },
                    
                    onPrePayoutSubmit: { [self] in
                        return await withCheckedContinuation { continuation in
                            Task { @MainActor in
                                self.pendingApprovalContinuation = continuation
                                self.showApprovalAlert = true
                            }
                        }
                    },
                    
                    onPostPayout: { [self] result in
                        print("Payout completed: \(result.merchantTransactionId)")
                        
                        Task { @MainActor in
                            completedTransactionId = result.merchantTransactionId
                            showSuccessAlert = true
                        }
                        
                        Analytics.track("direct_payout_completed", properties: [
                            "transactionId": result.merchantTransactionId,
                            "amount": payoutAmount
                        ])
                    },
                    
                    onError: { error in
                        guard let sdkError = error as? BaseSdkException else {
                            print("Payout error: \(error.localizedDescription)")
                            
                            Task { @MainActor in
                                showErrorAlert(error.localizedDescription)
                            }
                            
                            Analytics.track("direct_payout_error", properties: [
                                "errorCode": "UNKNOWN"
                            ])
                            return
                        }
                        
                        print("Payout error: \(sdkError.errorCode) - \(sdkError.errorMessage)")
                        
                        Task { @MainActor in
                            showErrorAlert(sdkError.errorMessage)
                        }
                        
                        Analytics.track("direct_payout_error", properties: [
                            "errorCode": sdkError.errorCode
                        ])
                    }
                )
                let submission = try pxpCheckout.create(.payoutSubmission, componentConfig: submissionConfig)
                
                await MainActor.run {
                    self.amountComponent = amount
                    self.receiverComponent = receiver
                    self.submissionComponent = submission
                    self.isLoading = false
                }
            } catch {
                print("Initialisation error: \(error.localizedDescription)")
                await MainActor.run {
                    self.isLoading = false
                }
            }
        }
    }
}

Error handling

The submission component can throw errors during component creation and during payout execution:

Component creation errors

These errors are thrown when calling pxpCheckout.create(.payoutSubmission, ...) and must be caught with try-catch:

Error codeExceptionDescription
SDK0803PaypalPayoutReceiverRequiredExceptionThe receiver is required.
SDK0804PayoutReceiverTypeInvalidExceptionInvalid receiver type.
SDK0806PayoutWalletInvalidExceptionInvalid wallet value. Only .paypal is currently supported.
SDK0807PayoutWalletRequiresEmailExceptionThe PayPal wallet configuration requires an email address. Both email and payerId must be provided in paypalConfig.payout.paypalWallet.
SDK0808PaypalPayoutEmailInvalidExceptionInvalid email format.
SDK0809PayoutPayerIdRequiredExceptionThe payer ID is required.
SDK0816PayoutEmailMaxLengthExceptionThe PayPal email address exceeds the maximum length.
SDK0817PayoutPayerIdMaxLengthExceptionThe payer ID exceeds 13 characters.
SDK0818PayoutPayerIdInvalidExceptionThe payer ID contains invalid characters.

Payout execution errors

These errors are passed to the onError callback during payout execution:

Error codeExceptionDescription
SDK0816PayoutEmailMaxLengthExceptionThe PayPal email address exceeds the maximum length.
SDK0817PayoutPayerIdMaxLengthExceptionThe payer ID exceeds 13 characters.
SDK0818PayoutPayerIdInvalidExceptionThe payer ID contains invalid characters.
SDK0819PayoutFailedExceptionThe payout transaction failed. May also include backend/API/HTTP error codes.

Amount and currency validation errors (SDK0810-SDK0815) belong to the PayoutAmountComponent and won't appear in submission component errors.

For a complete list of error codes, see Data validation.

Error handling example

Handling component creation errors

do {
    let config = PayoutSubmissionComponentConfig(
        submitText: "Withdraw to PayPal",
        onError: { error in
            // Handle execution-time errors
            handlePayoutError(error)
        }
    )
    
    let component = try pxpCheckout.create(.payoutSubmission, componentConfig: config)
    
} catch let error as BaseSdkException {
    // Handle creation-time errors
    let userMessage: String
    
    switch error.errorCode {
    case "SDK0803", "SDK0804", "SDK0808":
        userMessage = "Invalid PayPal account configuration. Please contact support."
        
    case "SDK0806":
        userMessage = "Invalid wallet type. Only PayPal wallets are currently supported."
        
    case "SDK0807":
        userMessage = "PayPal wallet configuration is incomplete. Both email and payer ID are required."
        
    case "SDK0809", "SDK0816", "SDK0817", "SDK0818":
        userMessage = "PayPal account information is invalid. Please re-link your account."
        
    default:
        userMessage = "Failed to initialize payout. Please contact support."
    }
    
    showErrorDialog(title: "Configuration Error", message: userMessage)
}

Handling Payout Execution Errors

config.onError = { error in
    guard let sdkError = error as? BaseSdkException else {
        Task { @MainActor in
            showErrorDialog(
                title: "Payout Error",
                message: "An error occurred. Please try again.",
                showRetryButton: true
            )
        }
        return
    }
    
    let userMessage: String
    let shouldRetry: Bool
    
    switch sdkError.errorCode {
    case "SDK0816", "SDK0817", "SDK0818":
        userMessage = "PayPal account information is invalid. Please re-link your account."
        shouldRetry = false
        
    case "SDK0819":
        userMessage = "Payout transaction failed. Please try again."
        shouldRetry = true
        
    default:
        // May include backend/API/HTTP error codes
        userMessage = "An error occurred. Please try again."
        shouldRetry = true
    }
    
    Task { @MainActor in
        showErrorDialog(
            title: "Payout Error",
            message: userMessage,
            showRetryButton: shouldRetry
        )
    }
}

Prerequisites

Before using the payout submission component, ensure you have:

  1. Configured the transaction data with a valid payout amount:
let payoutAmount: Decimal = 100.00

let transactionData = TransactionData(
    amount: payoutAmount,
    currency: "USD",
    entryType: .ecom,
    intent: TransactionIntentData(card: nil, paypal: .payout),
    merchantTransactionId: "payout-\(UUID().uuidString)",
    merchantTransactionDate: { Date() }
)
  1. Initialised the SDK with stored PayPal credentials:
let sessionData = SessionData(
    sessionId: "your-session-id",
    hmacKey: "your-hmac-key",
    encryptionKey: "your-encryption-key"
)

let paypalConfig = PayPalConfig(
    payout: PayPalPayoutConfig(
        paypalWallet: PayPalWallet(
            email: "user@example.com",        // Required: Stored from previous OAuth
            payerId: "PAYERID123ABC",         // Required: Stored from previous OAuth
            proceedPayoutWithSdk: true        // Required for payout execution
        )
    )
)

let checkoutConfig = CheckoutConfig(
    environment: .test,
    session: sessionData,
    transactionData: transactionData,
    merchantShopperId: "user-123",
    ownerId: "merchant-456",
    paypalConfig: paypalConfig
)

let pxpCheckout = try PxpCheckout.initialize(config: checkoutConfig)

Both paypalConfig.payout.paypalWallet.email and paypalConfig.payout.paypalWallet.payerId are required in CheckoutConfig before create(.payoutSubmission, ...) can succeed. These credentials should be stored from a previous PayPal OAuth flow.

For the SDK to execute payouts, all three conditions must be met:

  • proceedPayoutWithSdk must be set to true.
  • onPrePayoutSubmit callback must be provided in the component configuration.
  • onPrePayoutSubmit must return PrePayoutSubmitResult(isApproved: true).

If any condition isn't met, the payout won't execute.