Skip to content

Confirm payment flow

Authorise a payment, then capture it before it expires on iOS.

Overview

The confirm payment flow is designed for scenarios where you need to authorise payment first, then capture it after additional confirmation steps, making it ideal for complex orders, inventory checks, or when manual review is required.

If the funds aren't captured within the appropriate time limit (29 days), then the authorisation expires.

Payment flow

The PayPal confirm payment flow consists of six key steps with authorisation and capture separation.

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 for authorisation. If order creation fails, onError is triggered.

Step 3: PayPal approval

The customer is redirected to PayPal where they log in and approve the payment authorisation. PayPal handles all authentication and payment method selection within their secure environment.

This step has three associated callbacks:

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

Step 4: Return to merchant

After PayPal approval, the customer returns to your app with the authorised payment. The funds are authorised but not yet captured, giving you the opportunity to show order confirmation. This happens within your onApprove callback handler where you typically navigate to a confirmation screen.

Step 5: Order confirmation

The customer reviews their final order details on your confirmation screen. This is where you can perform final inventory checks, calculate shipping, or apply additional discounts.

Step 6: Payment capture

Once the customer confirms their order, you capture the authorised payment. This is when the funds are actually transferred from the customer's account.

Your capture API call will return a success or failure response, which you handle in your backend code.

Implementation

Before you start

To use the PayPal confirm payment flow 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. Prepare order confirmation screens for the authorisation-to-capture flow.
  5. Install and configure the PXP Checkout SDK for iOS.

Step 1: Configure your SDK for authorisation

Set up your SDK configuration with transaction information for a PayPal authorisation. The SDK uses the .authorisation intent for authorisation requests. For capturing funds, use .purchase intent.

The SDK uses .authorisation intent for authorisation requests, which PayPal maps to its "authorize" intent. For capturing funds, use .purchase intent, which maps to PayPal's "capture" intent.

import PXPCheckoutSDK

// FIRST INTENT: Authorization
let sessionData = SessionData(
    sessionId: "your-session-id",
    hmacKey: "your-hmac-key",
    encryptionKey: "your-encryption-key"
)

let transactionData = TransactionData(
    amount: 25.00,
    currency: "USD",
    entryType: .ecom,
    intent: TransactionIntentData(card: nil, paypal: .authorisation), // FIRST INTENT - for authorisation
    merchantTransactionId: "auth-\(UUID().uuidString)",
    merchantTransactionDate: { Date() }
)

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

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

Step 2: Implement authorisation callbacks

Implement the required callbacks for the PayPal confirm payment flow.

let config = PayPalButtonComponentConfig()
config.fundingSource = .paypal
config.payeeEmailAddress = "merchant@example.com"
config.paymentDescription = "Product purchase"
config.shippingPreference = .noShipping
config.userAction = .continue // For authorisation + confirmation flow

// REQUIRED: Handle successful payment authorisation
config.onApprove = { approvalData in
    print("Payment authorized: Order ID: \(approvalData.orderID), Payer ID: \(approvalData.payerID)")
    
    Task {
        do {
            // Store authorisation details
            let authResult = try await repository.storePayPalAuthorization(
                orderID: approvalData.orderID,
                payerID: approvalData.payerID,
                merchantTransactionId: transactionId,
                status: "authorized"
            )
            
            if authResult.isSuccess {
                print("Authorisation stored successfully")
                
                // Save to user defaults for confirmation screen
                    saveAuthorizationData(
                    orderID: approvalData.orderID,
                    payerID: approvalData.payerID,
                    authorizedAmount: transactionData.amount,
                    currency: transactionData.currency
                    )
                    
                    // Navigate to confirmation screen instead of completing payment
                await MainActor.run {
                    navigationCoordinator.navigateToConfirmation(orderID: approvalData.orderID)
                }
                } else {
                throw AuthorizationError.storageFailed(authResult.error ?? "Unknown error")
            }
        } catch {
            print("Authorisation processing error: \(error)")
            await MainActor.run {
                showError("Authorisation failed. Please try again.")
            }
        }
    }
}

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

    // OPTIONAL: Handle payment cancellation
config.onCancel = { error in
    print("Payment cancelled by user")
        showMessage("Payment was cancelled. Your cart is still saved.")
    }

Step 3: Render the authorisation component

Create and render the PayPal component for the authorisation step.

import SwiftUI
import PXPCheckoutSDK

struct PayPalAuthorizationView: View {
    @StateObject private var viewModel: PaymentViewModel
    @State private var paypalComponent: BaseComponent?
    
    var body: some View {
        VStack(spacing: 16) {
            Text("Authorize Payment")
                .font(.title)
            
            Text("Amount: $25.00")
                .font(.title2)
            
            if let component = paypalComponent {
                component.buildContent()
                    .frame(height: 50)
            } else {
                ProgressView()
                    .onAppear {
                        createPayPalComponent()
                    }
            }
        }
        .padding()
    }
    
    private func createPayPalComponent() {
        do {
            let config = PayPalButtonComponentConfig()
            config.fundingSource = .paypal
            config.userAction = .continue
            
            config.onApprove = { approvalData in
                viewModel.handleAuthorizationApproval(approvalData)
            }
            
            let component = try authPxpCheckout.create(
                .paypalButton,
                componentConfig: config
            )
            
            self.paypalComponent = component
        } catch {
            print("Failed to create PayPal component: \(error)")
        }
    }
}

Step 4: Handle order confirmation screen

Create a separate flow for handling the order confirmation and proceeding with the final capture.

import SwiftUI

struct OrderConfirmationView: View {
    let orderID: String
    @StateObject private var viewModel: PaymentViewModel
    @State private var showLoadingSpinner = false
    @State private var showSuccessDialog = false
    @State private var showErrorDialog = false
    @State private var errorMessage: String?
    @State private var authData: AuthorizationData?
    
    var body: some View {
        VStack(spacing: 16) {
            Text("Confirm Your Order")
                .font(.title)
        
        // Display order details
            if let authData = authData {
                GroupBox {
                    VStack(alignment: .leading, spacing: 8) {
                        Text("Order ID: \(orderID)")
                        Text("Amount: $\(String(format: "%.2f", authData.authorizedAmount))")
                        Text("Currency: \(authData.currency)")
                    }
            }
        }
        
        // Confirm button
            Button(action: {
                captureAuthorizedPayment()
            }) {
                if showLoadingSpinner {
                    ProgressView()
                        .progressViewStyle(CircularProgressViewStyle(tint: .white))
            } else {
                Text("Confirm and pay")
            }
        }
            .frame(maxWidth: .infinity)
            .frame(height: 50)
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(8)
            .disabled(showLoadingSpinner)
        }
        .padding()
        .onAppear {
            authData = viewModel.getAuthorizationData(orderID: orderID)
        }
        .alert("Payment Successful", isPresented: $showSuccessDialog) {
            Button("OK") {
                    showSuccessDialog = false
                // Navigate or dismiss
            }
        } message: {
            Text("Your payment has been captured successfully!")
        }
        .alert("Payment Error", isPresented: $showErrorDialog) {
            Button("OK") {
                showErrorDialog = false
            }
        } message: {
            Text(errorMessage ?? "An error occurred")
        }
    }
    
    private func captureAuthorizedPayment() {
        showLoadingSpinner = true
        
        Task {
            do {
                try await viewModel.captureAuthorizedPayment(orderID: orderID)
                
                await MainActor.run {
                    showLoadingSpinner = false
                    showSuccessDialog = true
                }
            } catch {
                await MainActor.run {
                    showLoadingSpinner = false
                    errorMessage = error.localizedDescription
                    showErrorDialog = true
                }
            }
        }
    }
}

Step 5: Implement payment capture

Implement the capture logic in your ViewModel.

class PaymentViewModel: ObservableObject {
    
    func captureAuthorizedPayment(orderID: String) async throws {
        // Retrieve authorisation data
        guard let authData = getAuthorizationData(orderID: orderID) else {
            throw CaptureError.authorizationNotFound
        }
                
                // Perform final checks (inventory, pricing, etc.)
        let orderValid = try await validateOrder(orderID: orderID)
        guard orderValid else {
            throw CaptureError.orderValidationFailed
                }
                
                // Calculate the final amount (may include shipping, taxes, discounts)
        let finalAmount = try await calculateFinalAmount(orderID: orderID)
                
                // Create new SDK configuration for capture
        let captureSessionData = SessionData(
            sessionId: "capture-session-id",
            hmacKey: "capture-hmac-key",
            encryptionKey: "capture-encryption-key"
        )
        
        let captureTransactionData = TransactionData(
            amount: finalAmount,
            currency: authData.currency,
            entryType: .ecom,
            intent: TransactionIntentData(card: nil, paypal: .purchase), // SECOND INTENT - for capture
            merchantTransactionId: "capture-\(UUID().uuidString)",
            merchantTransactionDate: { Date() }
        )
        
        let captureCheckoutConfig = CheckoutConfig(
            environment: .test,
            session: captureSessionData,
            transactionData: captureTransactionData,
            merchantShopperId: authData.merchantShopperId,
            ownerId: "owner-456"
        )
        
        let capturePxpCheckout = try PxpCheckout.initialize(config: captureCheckoutConfig)
        
        // Execute the capture
        let captureResult = try await repository.capturePayPalPayment(
            orderID: authData.orderID,
            payerID: authData.payerID,
            captureAmount: finalAmount
        )
        
        guard captureResult.status == "COMPLETED" else {
            throw CaptureError.captureFailed("Payment capture failed: \(captureResult.status)")
        }
        
        print("Payment captured successfully")
        
        // Store capture confirmation
        try await saveCaptureData(
            captureID: captureResult.captureID,
            transactionId: captureResult.transactionId,
            amount: captureResult.amount,
            status: "completed"
        )
    }
}

Step 6: Handle common scenarios

Inventory validation

Validate inventory between authorisation and capture.

func validateInventoryForCapture(orderID: String) async throws -> Bool {
    let inventoryCheck = try await repository.reserveInventory(
        orderID: orderID,
        items: getOrderItems(orderID: orderID),
        reservationDuration: 3600 // 1 hour reservation
    )
    
    if inventoryCheck.success {
        print("Inventory reserved for capture")
        return true
        } else {
        print("Inventory not available: \(inventoryCheck.message)")
        throw InventoryError.unavailable("Some items are no longer available. Please review your order.")
    }
}

Shipping calculation

Calculate final shipping costs during confirmation.

func calculateShippingAndTax(
    orderID: String,
    shippingAddress: ShippingAddress
) async throws -> FinalCalculation {
    let shippingResult = try await repository.calculateFinalShipping(
        orderID: orderID,
        shippingAddress: shippingAddress,
        items: getOrderItems(orderID: orderID)
    )
    
    guard shippingResult.success else {
        throw ShippingError.calculationFailed
    }
    
            // Show final total including shipping
    return FinalCalculation(
        subtotal: shippingResult.subtotal,
        shippingCost: shippingResult.shippingCost,
        tax: shippingResult.tax,
        total: shippingResult.total
    )
}

Example

The following example shows a complete PayPal confirm payment implementation with two separate intents.

Phase 1: Authorisation (checkout screen)

import SwiftUI
import PXPCheckoutSDK

struct PayPalAuthorizationScreen: View {
    @StateObject private var viewModel: PaymentViewModel
    @State private var paypalComponent: BaseComponent?
    @State private var showLoadingSpinner = false
    
    var body: some View {
        VStack(spacing: 16) {
            Text("Authorise payment")
                .font(.title)
            
            Text("Amount: $25.00")
                .font(.title2)
            
            // Mount the authorisation component
            if let component = paypalComponent {
                component.buildContent()
                    .frame(height: 50)
            } else {
                ProgressView()
            }
            
            if showLoadingSpinner {
                ProgressView()
            }
        }
        .padding()
        .onAppear {
            createAuthorizationComponent()
        }
    }
    
    private func createAuthorizationComponent() {
        do {
            // FIRST INTENT: Authorisation
            let sessionData = SessionData(
                sessionId: "auth-session-id",
                hmacKey: "auth-hmac-key",
                encryptionKey: "auth-encryption-key"
            )
            
            let transactionData = TransactionData(
                amount: 25.00,
                currency: "USD",
                entryType: .ecom,
                intent: TransactionIntentData(card: nil, paypal: .authorisation), // FIRST INTENT - for authorisation
                merchantTransactionId: "auth-\(UUID().uuidString)",
                merchantTransactionDate: { Date() }
            )
            
            let checkoutConfig = CheckoutConfig(
                environment: .test,
                session: sessionData,
                transactionData: transactionData,
                merchantShopperId: "shopper-123",
                ownerId: "owner-456"
            )
            
            let authPxpCheckout = try PxpCheckout.initialize(config: checkoutConfig)
            
            let config = PayPalButtonComponentConfig()
            config.fundingSource = .paypal
            config.payeeEmailAddress = "merchant@example.com"
            config.paymentDescription = "Product Purchase - Review Order"
            config.shippingPreference = .noShipping
            config.userAction = .continue // Authorisation + confirmation flow
            
            // Styling
            config.style = PayPalButtonStyleConfig(
                color: .gold,
                label: .paypal,
                size: .expanded,
                edges: .rounded
            )
            
            // Step 1: Handle payment authorisation
            config.onApprove = { approvalData in
                print("Processing PayPal authorisation")
                self.showLoadingSpinner = true
                
                Task {
                    do {
                        print("Order ID: \(approvalData.orderID)")
                        print("Authorised amount: $25.00")
                        
                        // Store authorisation data for capture step
                        try await viewModel.saveAuthorizationData(
                            orderID: approvalData.orderID,
                            payerID: approvalData.payerID,
                            merchantTransactionId: "auth-\(UUID().uuidString)",
                            authorizedAmount: 25.00,
                            currency: "USD"
                        )
                        
                        await MainActor.run {
                            self.showLoadingSpinner = false
                        // Navigate to confirmation screen
                            navigationCoordinator.navigateToConfirmation(orderID: approvalData.orderID)
                        }
                        
                    } catch {
                        print("Authorisation failed: \(error)")
                        await MainActor.run {
                            self.showLoadingSpinner = false
                            showError("Authorisation failed: \(error.localizedDescription)")
                        }
                    }
                }
            }
            
            config.onCancel = { error in
                print("PayPal authorisation cancelled by user")
                showMessage("Payment was cancelled. Your cart is still saved.")
            }
            
            config.onError = { error in
                print("PayPal authorisation error: \(error.errorMessage)")
                self.showLoadingSpinner = false
                showError("Authorisation failed. Please try again.")
            }
            
            let component = try authPxpCheckout.create(
                .paypalButton,
                componentConfig: config
            )
            
            self.paypalComponent = component
            
        } catch {
            print("Failed to create authorisation component: \(error)")
        }
    }
}

Phase 2: Capture (confirmation screen)

import SwiftUI
import PXPCheckoutSDK

struct PayPalCaptureScreen: View {
    let orderID: String
    @StateObject private var viewModel: PaymentViewModel
    @State private var showLoadingSpinner = false
    @State private var showSuccessDialog = false
    @State private var showErrorDialog = false
    @State private var errorMessage: String?
    @State private var authData: AuthorizationData?
    
    var body: some View {
        VStack(spacing: 16) {
            Text("Confirm Your Order")
                .font(.title)
            
            if let authData = authData {
                GroupBox {
                    VStack(alignment: .leading, spacing: 8) {
                        Text("Order ID: \(orderID)")
                        Text("Authorised: $\(String(format: "%.2f", authData.authorizedAmount))")
                    }
                }
            }
            
            Button(action: {
                captureAuthorizedPayment()
            }) {
                if showLoadingSpinner {
                    ProgressView()
                        .progressViewStyle(CircularProgressViewStyle(tint: .white))
                } else {
                    Text("Confirm and pay")
                }
            }
            .frame(maxWidth: .infinity)
            .frame(height: 50)
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(8)
            .disabled(showLoadingSpinner)
        }
        .padding()
        .onAppear {
            authData = viewModel.getAuthorizationData(orderID: orderID)
        }
        .alert("Payment Successful", isPresented: $showSuccessDialog) {
            Button("OK") {
                showSuccessDialog = false
            }
        }
        .alert("Payment Error", isPresented: $showErrorDialog) {
            Button("OK") {
                showErrorDialog = false
            }
        } message: {
            Text(errorMessage ?? "An error occurred")
        }
    }
    
    private func captureAuthorizedPayment() {
        guard let authData = authData else { return }
        
        showLoadingSpinner = true
        
        Task {
            do {
                // Calculate final amount (may be different due to shipping, taxes, etc.)
                let finalAmount = try await viewModel.calculateFinalAmount(orderID: orderID) // e.g., 27.50 with shipping
                
                // Create NEW SDK configuration for capture
                let captureSessionData = SessionData(
                    sessionId: "capture-session-id",
                    hmacKey: "capture-hmac-key",
                    encryptionKey: "capture-encryption-key"
                )
                
                let captureTransactionData = TransactionData(
                    amount: finalAmount,
                    currency: authData.currency,
                    entryType: .ecom,
                    intent: TransactionIntentData(card: nil, paypal: .purchase), // SECOND INTENT - for capture
                    merchantTransactionId: "capture-\(UUID().uuidString)",
                    merchantTransactionDate: { Date() }
                )
                
                let captureCheckoutConfig = CheckoutConfig(
                    environment: .test,
                    session: captureSessionData,
                    transactionData: captureTransactionData,
                    merchantShopperId: authData.merchantShopperId,
                    ownerId: "owner-456"
                )
                
                let capturePxpCheckout = try PxpCheckout.initialize(config: captureCheckoutConfig)
                
                // Execute the capture
                let captureResult = try await viewModel.repository.capturePayPalAuthorization(
                    authorizationOrderID: authData.orderID,
                    payerID: authData.payerID,
                    captureAmount: finalAmount
                )
                
                await MainActor.run {
                    self.showLoadingSpinner = false
                    
                    if captureResult.status == "COMPLETED" {
                        print("Payment captured successfully")
                        print("Captured Amount: $\(String(format: "%.2f", captureResult.amount))")
                        
                        // Store capture confirmation
                        viewModel.saveCaptureData(
                            captureID: captureResult.captureID,
                            transactionId: captureResult.transactionId,
                            amount: captureResult.amount,
                            status: "completed"
                        )
                        
                        self.showSuccessDialog = true
            } else {
                        throw CaptureError.captureFailed("Capture failed: \(captureResult.status)")
                    }
                }
                
            } catch {
                print("Capture process failed: \(error)")
                
                await MainActor.run {
                    self.showLoadingSpinner = false
                    
                    // Check for specific error conditions
                    let errorMessage = (error as? BaseSdkException)?.errorMessage ?? error.localizedDescription
                    if errorMessage.contains("expired") || errorMessage.contains("AUTHORIZATION_EXPIRED") {
                        self.errorMessage = "Authorisation expired. Please restart payment."
                    } else {
                        self.errorMessage = "Payment capture failed. Please contact support."
                    }
                    self.showErrorDialog = true
                }
            }
        }
    }
}

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 pay now 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()
}