Skip to content

Pay now flow

Capture funds instantly upon customer approval for iOS.

Overview

The pay now flow is designed for immediate payment capture, making it ideal for digital products, services, or when instant payment confirmation is required.

Payment flow

The PayPal pay now flow consists of five key steps for immediate payment processing.

Step 1: Submission

The customer taps the PayPal button, which triggers the payment flow. The SDK validates the PayPal configuration and transaction data before proceeding to order creation. If validation fails, onError is triggered.

Step 2: Order creation

The SDK creates a PayPal order using the provided transaction details. This involves sending the payment amount, currency, merchant information, and any additional order details to PayPal's API. If order creation fails, onError is triggered.

Step 3: PayPal approval

The customer is redirected to PayPal where they can log in and approve the payment.

This step has three associated callbacks:

  • onApprove: Proceeds with payment capture if the customer successfully approves the payment.
  • onCancel: Cancels the transaction if the customer cancels the payment.
  • onError: Receives error data if any error occurs during the approval process.

PayPal handles all authentication and payment method selection within their secure environment.

Step 4: Payment capture

Once the customer approves the payment, the SDK processes the payment immediately and automatically captures the funds. This happens within the onApprove callback handler, where you process the capture and handle success/failure.

Step 5: Payment result

You receive the final payment result from PayPal and the onApprove callback completes processing. The transaction is either completed successfully with payment confirmation, or fails with specific error details.

Implementation

Before you start

To use PayPal pay now payments in your iOS application:

  1. Ensure you have a valid PayPal Business account with API credentials.
  2. Configure your PayPal merchant account to accept the currencies you need.
  3. Set up your merchant configuration in the Unity Portal (API credentials, payment methods, risk settings).
  4. Install and configure the PXP Checkout SDK for iOS.

Step 1: Configure your SDK

Set up your SDK configuration with transaction information for PayPal payments. For pay now, use .purchase intent, which maps to PayPal's capture intent.

Intent for Pay Now: For immediate payment capture (pay now flow), use .purchase intent, which maps to PayPal's "capture" intent.

import PXPCheckoutSDK

// Create session data (obtained from your backend)
let sessionData = SessionData(
    sessionId: "your-session-id",
    hmacKey: "your-hmac-key",
    encryptionKey: "your-encryption-key"
)

// Create transaction data for pay now flow
let transactionData = TransactionData(
    amount: 25.00, // Payment amount
    currency: "USD",
    entryType: .ecom,
    intent: TransactionIntentData(card: nil, paypal: .purchase), // For immediate capture (pay now)
    merchantTransactionId: UUID().uuidString,
    merchantTransactionDate: { Date() }
)

// Create checkout configuration
let checkoutConfig = CheckoutConfig(
    environment: .test, // or .live
    session: sessionData,
    transactionData: transactionData,
    merchantShopperId: "shopper-123",
    ownerId: "owner-456"
)

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

Step 2: Implement callbacks

Implement the required callbacks for PayPal pay now payments.

// Create PayPal component configuration
let config = PayPalButtonComponentConfig()
config.fundingSource = .paypal
config.shippingPreference = .noShipping
config.userAction = .payNow
config.payeeEmailAddress = "merchant@example.com"
config.paymentDescription = "Product purchase"

// REQUIRED: Handle successful payment approval
config.onApprove = { approvalData in
    print("Payment approved: Order ID: \(approvalData.orderID), Payer ID: \(approvalData.payerID)")
    
    // Process the payment on your backend
    Task {
        do {
            let result = try await yourBackendService.capturePayPalPayment(
                orderID: approvalData.orderID,
                payerID: approvalData.payerID,
                merchantTransactionId: transactionId
            )
            
            if result.status == "COMPLETED" {
                print("Payment captured successfully: \(result.id)")
                
                // Navigate to success screen
                await MainActor.run {
                    navigationCoordinator.navigateToSuccess(orderID: approvalData.orderID)
                }
            } else {
                throw PaymentError.captureFailed(result.error ?? "Unknown error")
            }
        } catch {
            print("Payment processing error: \(error.localizedDescription)")
            await MainActor.run {
                showError("Payment failed. Please try again.")
            }
        }
    }
}

// REQUIRED: Handle payment errors
config.onError = { error in
    print("Payment error: \(error.errorMessage)")
    showError("Payment failed. Please try again.")
}

// OPTIONAL: Handle payment cancellation
config.onCancel = { error in
    print("Payment cancelled by user")
    showMessage("Payment was cancelled. You can try again anytime.")
}

Step 3: Render the component

Create and render the PayPal component in your SwiftUI view.

import SwiftUI
import PXPCheckoutSDK

struct PayPalPaymentView: View {
    @StateObject private var viewModel: PaymentViewModel
    @State private var paypalComponent: BaseComponent?
    
    var body: some View {
        VStack(spacing: 16) {
            Text("Complete your payment")
                .font(.title)
            
            Text("Amount: $25.00")
                .font(.title2)
            
            // PayPal button
            if let component = paypalComponent {
                component.buildContent()
                    .frame(height: 50)
            } else {
                ProgressView()
                    .onAppear {
                        createPayPalComponent()
                    }
            }
        }
        .padding()
    }
    
    private func createPayPalComponent() {
        do {
            let config = PayPalButtonComponentConfig()
            config.fundingSource = .paypal
            
            // Configure callbacks
            config.onApprove = { approvalData in
                viewModel.handlePaymentApproval(approvalData)
            }
            config.onError = { error in
                viewModel.handlePaymentError(error)
            }
            config.onCancel = { error in
                viewModel.handlePaymentCancellation()
            }
            
            // Create component
            let component = try pxpCheckout.create(
                .paypalButton,
                componentConfig: config
            )
            
            self.paypalComponent = component
        } catch {
            print("Failed to create PayPal component: \(error)")
        }
    }
}

Step 4: Handle common scenarios

Amount-based processing

Use different processing logic based on transaction amounts.

config.onApprove = { approvalData in
    let amount = transactionData.amount
    
    // Add additional verification for high-value transactions
    if amount > 100.00 { // Over $100.00
        Task {
            let confirmed = await showConfirmationDialog(
                message: "Confirm payment of $\(amount)?"
            )
            
            if confirmed {
                await processPayPalPayment(approvalData)
            } else {
                await MainActor.run {
                    showMessage("Payment cancelled by user.")
                }
            }
        }
    } else {
        Task {
            await processPayPalPayment(approvalData)
        }
    }
}

Customer type handling

Handle different customer types with varying processing requirements.

enum CustomerType {
    case new, returning, vip
}

struct ProcessingOptions {
    let verificationLevel: VerificationLevel
    let emailConfirmation: Bool
    let fastProcessing: Bool
}

func getCustomerProcessingOptions(customerType: CustomerType) -> ProcessingOptions {
    switch customerType {
    case .new:
        return ProcessingOptions(
            verificationLevel: .enhanced,
            emailConfirmation: true,
            fastProcessing: false
        )
    case .returning:
        return ProcessingOptions(
            verificationLevel: .standard,
            emailConfirmation: false,
            fastProcessing: false
        )
    case .vip:
        return ProcessingOptions(
            verificationLevel: .minimal,
            emailConfirmation: false,
            fastProcessing: true
        )
    }
}

config.onApprove = { approvalData in
    let processingOptions = getCustomerProcessingOptions(customerType: .returning)
    
    Task {
        await processPayPalPayment(
            approvalData: approvalData,
            options: processingOptions,
            customerType: .returning
        )
    }
}

Step 5: Handle errors

Implement comprehensive error handling for PayPal payments.

config.onError = { error in
    print("Error: \(error.errorMessage)")
    
    // Check error code
    let errorCode = error.errorCode
    let errorMessage = error.errorMessage
    
    if errorCode == "PAYPAL_ERROR" {
        // PayPal-specific errors - check message for details
        if errorMessage.contains("declined") || errorMessage.contains("INSTRUMENT_DECLINED") {
            showErrorDialog(
                title: "Payment Declined",
                message: "Your PayPal payment method was declined. Please try a different method."
            )
        } else if errorMessage.contains("verification") || errorMessage.contains("PAYER_ACTION_REQUIRED") {
            showErrorDialog(
                title: "Action Required",
                message: "Additional action required in PayPal. Please complete the payment process."
            )
        } else {
            showErrorDialog(
                title: "Payment Error",
                message: "Payment failed: \(errorMessage)"
            )
        }
    } else if errorCode == "VALIDATION_FAILED" {
        showErrorDialog(
            title: "Validation Error",
            message: "Payment details validation failed. Please try again."
        )
    } else {
        showErrorDialog(
            title: "Payment Error",
            message: "Payment failed. Please try again or contact support."
        )
    }
}

config.onApprove = { approvalData in
    Task {
        do {
            try await processPayPalPayment(approvalData)
        } catch let error as HTTPError {
            // Handle backend processing errors
            switch error.statusCode {
            case 422:
                await MainActor.run {
                    showError("Payment details could not be verified. Please try again.")
                }
            case 409:
                await MainActor.run {
                    showError("This payment has already been processed.")
                }
            case 500...599:
                await MainActor.run {
                    showError("Payment system temporarily unavailable. Please try again later.")
                }
            default:
                await MainActor.run {
                    showError("Payment processing failed. Please try again.")
                }
            }
        }
    }
}

Example

The following example shows a complete PayPal pay now implementation with SwiftUI.

import SwiftUI
import PXPCheckoutSDK

struct PayPalPayNowView: View {
    @StateObject private var viewModel: PaymentViewModel
    @State private var paypalComponent: BaseComponent?
    @State private var showLoadingSpinner = false
    @State private var showSuccessDialog = false
    @State private var showErrorDialog = false
    @State private var paymentResult: PaymentResult?
    @State private var errorMessage: String?
    
    var body: some View {
        VStack(spacing: 16) {
            Text("Complete your payment")
                .font(.title)
            
            Text("Amount: $25.00")
                .font(.title2)
            
            // PayPal button
            if let component = paypalComponent {
                component.buildContent()
                    .frame(height: 50)
            } else {
                ProgressView()
            }
            
            // Loading indicator
            if showLoadingSpinner {
                ProgressView()
                    .scaleEffect(1.5)
            }
        }
        .padding()
        .onAppear {
            createPayPalComponent()
        }
        .alert("Payment Successful", isPresented: $showSuccessDialog) {
            Button("OK") {
                showSuccessDialog = false
                // Navigate or dismiss
            }
        } message: {
            Text("Your payment has been processed successfully!")
        }
        .alert("Payment Error", isPresented: $showErrorDialog) {
            Button("OK") {
                showErrorDialog = false
            }
        } message: {
            Text(errorMessage ?? "An error occurred")
        }
    }
    
    private func createPayPalComponent() {
        do {
            let config = PayPalButtonComponentConfig()
            
            // PayPal configuration
            config.fundingSource = .paypal
            config.payeeEmailAddress = "merchant@example.com"
            config.paymentDescription = "Product Purchase - $25.00"
            config.shippingPreference = .noShipping
            config.userAction = .payNow
            
            // Styling
            config.style = PayPalButtonStyleConfig(
                color: .gold,
                label: .paypal,
                size: .expanded,
                edges: .rounded
            )
            
            // Step 1: Handle payment approval
            config.onApprove = { approvalData in
                print("Processing PayPal payment")
                self.showLoadingSpinner = true
                
                Task {
                    do {
                        print("Order ID: \(approvalData.orderID)")
                        print("Payer ID: \(approvalData.payerID)")
                        
                        // Process payment on backend
                        let result = try await viewModel.capturePayment(
                            orderID: approvalData.orderID,
                            payerID: approvalData.payerID
                        )
                        
                        await MainActor.run {
                            self.showLoadingSpinner = false
                            
                            if result.isSuccess {
                                print("Payment completed successfully")
                                self.paymentResult = result.data
                                self.showSuccessDialog = true
                            } else {
                                self.errorMessage = result.error ?? "Payment capture failed"
                                self.showErrorDialog = true
                            }
                        }
                    } catch {
                        print("Payment processing failed: \(error)")
                        await MainActor.run {
                            self.showLoadingSpinner = false
                            self.errorMessage = error.localizedDescription
                            self.showErrorDialog = true
                        }
                    }
                }
            }
            
            // Step 2: Handle cancellation
            config.onCancel = { error in
                print("Payment cancelled by user")
                // Show user-friendly message
            }
            
            // Step 3: Handle errors
            config.onError = { error in
                print("Payment error: \(error.errorMessage)")
                self.showLoadingSpinner = false
                self.errorMessage = error.errorMessage
                self.showErrorDialog = true
            }
            
            // Create component
            let component = try pxpCheckout.create(
                .paypalButton,
                componentConfig: config
            )
            
            self.paypalComponent = component
            
        } catch {
            print("Failed to create PayPal component: \(error)")
        }
    }
}

Callback data

This section describes the data received by the different callbacks as part of the PayPal pay now flow. This data is the same as for the confirm payment flow.

onApprove

The onApprove callback receives payment approval data when the customer successfully approves the payment in PayPal. The approval data includes the PayPal order ID and payer information needed to capture the payment.

{
  "orderID": "7YH53119ML8957234",
  "payerID": "ABCDEFGHIJKLM"
}
ParameterDescription
orderID
String
required
The unique PayPal order ID that identifies this payment.
payerID
String
required
The unique PayPal payer ID that identifies the customer who approved the payment.

Here's an example of what to do with this data:

config.onApprove = { approvalData in
    print("Payment approved: \(approvalData)")
    
    Task {
        do {
            // Capture the payment immediately for pay now flow
            let captureResponse = try await repository.capturePayPalPayment(
                orderID: approvalData.orderID,
                payerID: approvalData.payerID,
                merchantTransactionId: UUID().uuidString,
                timestamp: Date()
            )
            
            if captureResponse.status == "COMPLETED" {
                // Payment captured successfully
                print("Payment captured: \(captureResponse.id)")
                
                // Store transaction record
                try await database.insertTransaction(
                    Transaction(
                        paypalOrderId: approvalData.orderID,
                        paypalPayerId: approvalData.payerID,
                        transactionId: captureResponse.id,
                        amount: captureResponse.amount,
                        currency: captureResponse.currency,
                        status: "completed",
                        timestamp: Date()
                    )
                )
                
                // Navigate to success
                await MainActor.run {
                    navigationCoordinator.navigateToSuccess(orderID: approvalData.orderID)
                }
            } else {
                throw PaymentError.captureFailed("Payment capture failed: \(captureResponse.status)")
            }
        } catch {
            print("Payment capture error: \(error)")
            await MainActor.run {
                showError("Payment processing failed. Please contact support.")
            }
        }
    }
}

onError

The onError callback receives error information when PayPal payments fail. PayPal errors include specific error names and details to help with troubleshooting.

{
  "name": "VALIDATION_ERROR",
  "message": "Invalid payment method",
  "details": [{
    "issue": "INSTRUMENT_DECLINED",
    "description": "The instrument presented was either declined by the processor or bank, or it can't be used for this payment."
  }]
}
ParameterDescription
name
String
The error name.

Possible values:
  • VALIDATION_ERROR
  • INSTRUMENT_DECLINED
  • PAYER_ACTION_REQUIRED
  • UNPROCESSABLE_ENTITY
message
String
A human-readable error message.

Here's an example of how to handle PayPal errors:

config.onError = { error in
    print("Payment error: \(error.errorMessage)")
    
    // Check error code
    let errorCode = error.errorCode
    let errorMessage = error.errorMessage
    
    if errorCode == "PAYPAL_ERROR" {
        // PayPal-specific errors - check message for details
        if errorMessage.contains("declined") || errorMessage.contains("INSTRUMENT_DECLINED") {
            showErrorDialog(
                title: "Payment Declined",
                message: "Your PayPal payment method was declined. Please try a different payment method."
            )
        } else if errorMessage.contains("verification") || errorMessage.contains("PAYER_ACTION_REQUIRED") {
            showErrorDialog(
                title: "Action Required",
                message: "Additional verification required. Please complete the process in PayPal."
            )
        } else {
            showErrorDialog(
                title: "Payment Error",
                message: "Payment failed: \(errorMessage)"
            )
        }
    } else if errorCode == "VALIDATION_FAILED" {
        showErrorDialog(
            title: "Validation Error",
            message: "Payment information is invalid. Please try again."
        )
    } else {
        showErrorDialog(
            title: "Payment Error",
            message: "Payment failed. Please try again or contact support."
        )
    }
    
    // Log error details for monitoring
    Analytics.logEvent("paypal_payment_error", parameters: [
        "error_code": errorCode,
        "error_message": errorMessage,
        "timestamp": Date().timeIntervalSince1970,
        "payment_method": "paypal"
    ])
}

onCancel

The onCancel callback receives error information when the customer cancels the PayPal payment process.

Here's an example of how to handle cancellations:

config.onCancel = { error in
    print("Payment cancelled by user")
    
    // Log cancellation for analytics
    Analytics.logEvent("paypal_payment_cancelled", parameters: [
        "timestamp": Date().timeIntervalSince1970,
        "payment_method": "paypal"
    ])
    
    // Show user-friendly message
    showMessage("Payment was cancelled. Your cart items are still saved.")
    
    // Optional: Offer alternative payment methods
    showAlternativePaymentOptions()
}