Skip to content

Troubleshooting

Learn how to diagnose and fix common issues with Checkout Drop-in.

Quick diagnostics

If you're experiencing issues with Drop-in, start with these quick diagnostic checks:

import PXPCheckoutSDK
import UIKit
import Network

// Drop-in diagnostic helper
func diagnoseDropIn(
    sessionData: SessionData?,
    config: CheckoutDropInConfig
) {
    print("=== Checkout Drop-in diagnostics ===")
    
    // Check SDK import
    print("SDK imported: \(CheckoutDropIn.self)")
    
    // Check session data
    print("Session ID: \(sessionData?.sessionId != nil ? "Present" : "Missing")")
    print("HMAC key: \(sessionData?.hmacKey != nil ? "Present" : "Missing")")
    print("Encryption key: \(sessionData?.encryptionKey != nil ? "Present" : "Missing")")
    if let sessionExpiry = sessionData?.sessionExpiry {
        print("Session expiry: \(sessionExpiry)")
    }
    if let restrictions = sessionData?.restrictions {
        print("Restrictions: \(String(describing: restrictions))")
    }
    print("Allowed funding types: \(String(describing: sessionData?.allowedFundingTypes))")
    
    // Check environment
    print("Environment: \(config.environment)")
    print("Owner ID: \(config.ownerId)")
    print("Merchant Shopper ID: \(config.merchantShopperId)")
    
    // Check device info
    print("iOS version: \(UIDevice.current.systemVersion)")
    print("Device: \(UIDevice.current.model)")
    print("Screen scale: \(UIScreen.main.scale)")
    
    // Check network connectivity
    let monitor = NWPathMonitor()
    let queue = DispatchQueue(label: "NetworkMonitor")
    monitor.pathUpdateHandler = { path in
        print("Network available: \(path.status == .satisfied)")
    }
    monitor.start(queue: queue)
    
    print("===================================")
}

Common issues

Drop-in not rendering

Symptoms:

  • The component doesn't appear in your SwiftUI view.
  • No payment methods are visible.
  • There are no errors in the console.
CauseSolution
create() not completed.Always await dropIn.create() before rendering buildContent(). Until create() finishes, the SDK returns EmptyView() with no thrown error. If getCheckoutDropInConfig fails, create() returns early and fires onError (for example SDK1100 or SDK0204) — without a wired onError handler, failures look like a blank drop-in with no console error.
SwiftUI view update issue.Ensure the session data is loaded before rendering CheckoutDropIn. Use .task or onAppear to fetch session data and conditional rendering.
Session data invalid.Verify that the session data includes sessionId, hmacKey, and allowedFundingTypes. Check the backend session creation logs.
No payment methods enabled.Check allowedFundingTypes in your session. At least one payment method must be enabled in the Unity Portal.
Incorrect configuration.Verify environment and ownerId are set correctly.
SDK not properly linked.Verify that the Swift Package Manager dependency is properly resolved in Xcode.

Diagnostic steps:

import SwiftUI
import PXPCheckoutSDK

struct DiagnoseRenderingIssueView: View {
    @StateObject private var viewModel = DiagnosticViewModel()
    
    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 8) {
                Text(viewModel.diagnostics)
                    .font(.system(.caption, design: .monospaced))
                
                if let dropIn = viewModel.dropIn {
                    dropIn.buildContent()
                } else {
                    ProgressView("Loading...")
                }
            }
            .padding()
        }
        .task {
            await viewModel.diagnose()
        }
    }
}

@MainActor
final class DiagnosticViewModel: ObservableObject {
    @Published var dropIn: CheckoutDropIn?
    @Published var diagnostics: String = ""
    
    func diagnose() async {
        diagnostics += "Step 1: Fetching session...\n"
        
        do {
            let sessionData = try await fetchSessionFromBackend()
            diagnostics += "Step 2: Session fetched successfully\n"
            diagnostics += "Session ID: \(sessionData.sessionId)\n"
            
            // Step 3: Validate session data
            diagnostics += "Step 3: Validating session data...\n"
            
            if sessionData.sessionId.isEmpty {
                diagnostics += "ERROR: Session ID missing\n"
            } else {
                diagnostics += "Session ID present\n"
            }
            
            if sessionData.allowedFundingTypes == nil {
                diagnostics += "ERROR: No allowed funding types\n"
            } else {
                var paymentMethodCount = 0
                
                // Cards: must not be nil
                if sessionData.allowedFundingTypes?.cards != nil {
                    paymentMethodCount += 1
                }
                
                // PayPal: must have allowedFundingOptions present and non-empty
                if let paypalConfig = sessionData.allowedFundingTypes?.wallets?.paypal,
                   let allowedFundingOptions = paypalConfig.allowedFundingOptions,
                   !allowedFundingOptions.isEmpty {
                    paymentMethodCount += 1
                }
                
                // Apple Pay: must have merchantId present and non-empty
                if let applePayConfig = sessionData.allowedFundingTypes?.wallets?.applePay,
                   let merchantId = applePayConfig.merchantId,
                   !merchantId.isEmpty {
                    paymentMethodCount += 1
                }
                
                if paymentMethodCount == 0 {
                    diagnostics += "ERROR: No payment methods enabled (SDK0204)\n"
                } else {
                    diagnostics += "\(paymentMethodCount) payment method(s) available\n"
                }
            }
            
            // Initialise Drop-in
            let config = CheckoutDropInConfig(
                environment: .test,
                session: sessionData,
                transactionData: DropInTransactionData(
                    amount: Decimal(string: "1.00") ?? 0,
                    currency: "USD",
                    entryType: .ecom,
                    intent: DropInTransactionIntentData(
                        card: .authorisation
                    ),
                    merchantTransactionId: "test-\(Date().timeIntervalSince1970)",
                    merchantTransactionDate: { Date() }
                ),
                merchantShopperId: "shopper-123",
                ownerId: "MERCHANT-1",
                onSuccess: { result in
                    print("Success: \(result.systemTransactionId)")
                },
                onError: { paymentMethod, error in
                    print("Error: \(error.errorMessage)")
                }
            )
            
            let instance = try CheckoutDropIn(config: config)
            await instance.create()
            dropIn = instance
            
        } catch {
            diagnostics += "ERROR: \(error.localizedDescription)\n"
            print("Diagnostic error: \(error)")
        }
    }
}

Session expired errors

Symptoms:

  • A "Session expired" error message is displayed.
  • The drop-in loads but payment fails immediately.
  • Console shows session timeout errors.
CauseSolution
Session has expired.Sessions expire based on your backend configuration. Check the optional sessionExpiry field returned by your backend when creating the session. Create a new session when needed.
Clock skewEnsure that the server and device clocks are synchronised.
Session reusedTreat sessions as checkout-attempt scoped unless your backend explicitly supports reuse. Always fetch a fresh session for new checkout attempts.
Invalid HMAC signatureVerify that the HMAC key matches between session creation and SDK initialisation.

Solution: Implement session refresh

import SwiftUI
import PXPCheckoutSDK

struct CheckoutViewWithSessionRefresh: View {
    @StateObject private var viewModel = CheckoutViewModel()
    
    var body: some View {
        Group {
            if let dropIn = viewModel.dropIn {
                dropIn.buildContent()
            } else if let errorMessage = viewModel.errorMessage {
                VStack {
                    Text(errorMessage)
                        .foregroundColor(.red)
                    
                    Button("Refresh") {
                        Task {
                            await viewModel.refreshSession()
                        }
                    }
                }
            } else {
                ProgressView("Loading checkout...")
            }
        }
        .task {
            await viewModel.loadDropIn()
        }
    }
}

@MainActor
final class CheckoutViewModel: ObservableObject {
    @Published var dropIn: CheckoutDropIn?
    @Published var errorMessage: String?
    
    func loadDropIn() async {
        do {
            let sessionData = try await fetchSessionFromBackend()
            try await initializeDropIn(with: sessionData)
        } catch {
            print("Failed to fetch session: \(error)")
            errorMessage = "Failed to load checkout. Please try again."
        }
    }
    
    func refreshSession() async {
        errorMessage = nil
        dropIn = nil
        await loadDropIn()
    }
    
    private func initializeDropIn(with sessionData: SessionData) async throws {
        let config = CheckoutDropInConfig(
            environment: .live,
            session: sessionData,
            transactionData: DropInTransactionData(
                amount: Decimal(string: "99.99") ?? 0,
                currency: "USD",
                entryType: .ecom,
                intent: DropInTransactionIntentData(
                    card: .authorisation
                ),
                merchantTransactionId: UUID().uuidString,
                merchantTransactionDate: { Date() }
            ),
            merchantShopperId: "shopper-123",
            ownerId: "MERCHANT-1",
            onSuccess: { result in
                Task {
                    await verifyPaymentOnBackend(result)
                }
            },
            onError: { paymentMethod, error in
                // Handle session expiry
                if error.errorMessage.localizedCaseInsensitiveContains("expired") ||
                   error.errorMessage.localizedCaseInsensitiveContains("session") {
                    print("Session expired, refreshing...")
                    
                    Task { @MainActor in
                        self.errorMessage = "Session expired. Please refresh."
                    }
                } else {
                    print("Payment error: \(error.errorMessage)")
                    Task { @MainActor in
                        self.errorMessage = "Payment failed: \(error.errorMessage)"
                    }
                }
            }
        )
        
        let instance = try CheckoutDropIn(config: config)
        await instance.create()
        dropIn = instance
    }
}

func fetchSessionFromBackend() async throws -> SessionData {
    // Implementation to fetch session from your backend
    // ...
}

Payment method not appearing

Symptoms:

  • An expected payment method isn't shown.
  • Only the card payment method is visible.
  • Apple Pay doesn't appear even though it's enabled.
Payment methodCommon causesSolutions
CardsCards aren't enabled in the Unity Portal or are missing from the session configuration.Enable the Card service in the Unity Portal and verify that the session includes allowedFundingTypes.cards.
PayPalPayPal onboarding wasn't completed or is missing from session configuration.Complete PayPal onboarding in Unity Portal and verify that the session includes allowedFundingTypes.wallets.paypal.
Apple PayApple Pay not configured in Xcode, no cards in Apple Wallet, or device doesn't support Apple Pay.Enable Apple Pay capability in Xcode, add test cards to Wallet, and test on a physical device or simulator with Apple Pay support.

Diagnostic steps:

import PassKit
import PXPCheckoutSDK

func diagnosePaymentMethodVisibility(
    sessionData: SessionData?,
    config: CheckoutDropInConfig
) {
    print("=== Payment method diagnostics ===")
    
    let fundingTypes = sessionData?.allowedFundingTypes
    print("Session funding types: \(String(describing: fundingTypes))")
    
    // Check cards - must not be nil
    if fundingTypes?.cards != nil {
        print("Cards enabled: \(String(describing: fundingTypes?.cards))")
    } else {
        print("WARNING: Cards not enabled in session (allowedFundingTypes.cards must not be nil)")
    }
    
    // Check PayPal - must have allowedFundingOptions present and non-empty
    if let paypalConfig = fundingTypes?.wallets?.paypal,
       let allowedFundingOptions = paypalConfig.allowedFundingOptions,
       !allowedFundingOptions.isEmpty {
        print("PayPal enabled with funding options: \(allowedFundingOptions)")
    } else {
        print("WARNING: PayPal not enabled (wallets.paypal.allowedFundingOptions must be present and non-empty)")
    }
    
    // Check Apple Pay - must have merchantId present and non-empty
    if let applePayConfig = fundingTypes?.wallets?.applePay,
       let merchantId = applePayConfig.merchantId,
       !merchantId.isEmpty {
        print("Apple Pay configuration present with merchant ID: \(merchantId)")
        
        // Check Apple Pay availability
        if PKPaymentAuthorizationController.canMakePayments() {
            print("Device supports Apple Pay")
            
            // Check if user has cards configured for networks from Drop-in config
            let networks = config.methodConfig?.global?.acceptedCardNetworks?.compactMap { network -> PKPaymentNetwork? in
                switch network {
                case .visa: return .visa
                case .mastercard: return .masterCard
                case .amex: return .amex
                case .discover: return .discover
                default: return nil
                }
            } ?? [.visa, .masterCard, .amex]
            
            if PKPaymentAuthorizationController.canMakePayments(usingNetworks: networks) {
                print("Apple Pay has cards configured for accepted networks: \(networks)")
            } else {
                print("WARNING: No cards configured in Apple Pay for accepted networks")
            }
        } else {
            print("WARNING: Device does not support Apple Pay")
        }
    } else {
        print("WARNING: Apple Pay not configured (wallets.applePay.merchantId must be present and non-empty)")
    }
    
    // Check device info
    print("iOS version: \(UIDevice.current.systemVersion)")
    print("Device: \(UIDevice.current.model)")
    
    print("===================================")
}

Apple Pay not working

Symptoms:

  • Apple Pay button doesn't appear.
  • Apple Pay sheet doesn't open when tapped.
  • Payment fails with Apple Pay errors.
CauseSolution
Apple Pay capability not enabled.Enable Apple Pay capability in Xcode project settings.
No cards in Apple Wallet.Add test cards to Apple Wallet manually.
Minimum iOS version not met.Apple Pay requires iOS 14.0 or higher.
Merchant ID missing or invalid.Add a valid Apple Pay merchant ID in your Xcode project and Unity Portal.
Device not supported.Use a physical device for final validation. Simulator is useful for UI checks only but may not fully represent production Apple Pay behaviour.

Solution:

import PassKit

func checkApplePayAvailability() {
    print("=== Apple Pay Diagnostics ===")
    
    // Check if device supports Apple Pay
    if PKPaymentAuthorizationController.canMakePayments() {
        print("✓ Device supports Apple Pay")
    } else {
        print("✗ Device does not support Apple Pay")
        return
    }
    
    // Check if user has cards configured
    let networks: [PKPaymentNetwork] = [.visa, .masterCard, .amex]
    if PKPaymentAuthorizationController.canMakePayments(usingNetworks: networks) {
        print("✓ Apple Pay has cards configured")
    } else {
        print("✗ No cards configured in Apple Wallet")
        print("  Add cards to Wallet to use Apple Pay")
    }
    
    // Check iOS version
    if #available(iOS 14.0, *) {
        print("✓ iOS version compatible (14.0+)")
    } else {
        print("✗ iOS version too old (requires 14.0+)")
    }
    
    print("=============================")
}

Backend verification failing

Symptoms:

  • Frontend onSuccess fires but backend verification fails.
  • Orders aren't being fulfilled.
  • "Payment verification failed" errors.
CauseSolution
Webhook not configured.Set up a webhook URL in the Unity Portal and implement a webhook handler on your backend.
Webhook authentication failing.Verify your webhook signature/authentication. Check your HMAC implementation.
Race condition (GET before webhook).Implement a fallback to the Get transaction details API if the webhook hasn't arrived yet.
Amount mismatch.Ensure that the amount in the verification request exactly matches the transaction amount.
Transaction ID mismatchVerify that the systemTransactionId and merchantTransactionId match database records.

Solution: Robust backend verification

// Backend webhook handler (Node.js/Express example)
app.post('/webhooks/pxp', async (req, res) => {
  try {
    const events = req.body;
    
    // Verify webhook authenticity using HMAC
    if (!verifyWebhookSignature(req)) {
      console.error('Invalid webhook signature');
      return res.status(401).json({ error: 'Unauthorised' });
    }
    
    for (const event of events) {
      if (event.eventCategory === 'Transaction') {
        const txn = event.eventData;
        
        console.log('Processing transaction:', txn.systemTransactionId);
        
        // Idempotency check
        const existing = await db.transactions.findOne({
          systemTransactionId: txn.systemTransactionId
        });
        
        if (existing) {
          console.log('Transaction already processed, skipping');
          continue;
        }
        
        // Verify transaction state
        if (txn.state === 'Authorised' || txn.state === 'Captured') {
          // Find order by merchant transaction ID
          const order = await db.orders.findOne({
            merchantTransactionId: txn.merchantTransactionId
          });
          
          if (!order) {
            console.error('Order not found:', txn.merchantTransactionId);
            continue;
          }
          
          // Verify amount matches
          const transactionAmount = txn.amounts?.transactionValue || txn.amount;
          if (Math.abs(transactionAmount - order.amount) > 0.01) {
            console.error('Amount mismatch:', {
              expected: order.amount,
              actual: transactionAmount
            });
            continue;
          }
          
          // Mark transaction as processed
          await db.transactions.create({
            systemTransactionId: txn.systemTransactionId,
            merchantTransactionId: txn.merchantTransactionId,
            amount: transactionAmount,
            state: txn.state,
            processedAt: new Date()
          });
          
          // Fulfill order
          await fulfillOrder(order.id, txn.systemTransactionId);
          
          console.log('Order fulfilled:', order.id);
        }
      }
    }
    
    // Always return success
    res.json({ state: 'Success' });
  } catch (error) {
    console.error('Webhook processing error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

iOS-specific issues

Memory leaks

Symptoms:

  • App performance degrades over time.
  • Xcode Instruments shows growing memory usage.
  • Out of memory crashes after multiple checkout attempts.

Solution: Proper lifecycle management

import SwiftUI
import PXPCheckoutSDK

struct CheckoutView: View {
    @StateObject private var viewModel = CheckoutViewModel()
    @Environment(\.scenePhase) private var scenePhase
    
    var body: some View {
        Group {
            if let dropIn = viewModel.dropIn {
                dropIn.buildContent()
            } else if let errorMessage = viewModel.errorMessage {
                Text(errorMessage)
                    .foregroundColor(.red)
            } else {
                ProgressView("Loading...")
            }
        }
        .task {
            await viewModel.loadDropIn()
        }
        .onChange(of: scenePhase) { oldPhase, newPhase in
            switch newPhase {
            case .background:
                print("App backgrounded")
            case .inactive:
                print("App inactive")
            case .active:
                print("App active")
            @unknown default:
                break
            }
        }
        .onDisappear {
            viewModel.cleanup()
        }
    }
}

@MainActor
final class CheckoutViewModel: ObservableObject {
    @Published var dropIn: CheckoutDropIn?
    @Published var errorMessage: String?
    
    func loadDropIn() async {
        // Implementation
    }
    
    func cleanup() {
        dropIn?.destroy()
        dropIn = nil
        errorMessage = nil
        print("Checkout view cleaned up")
    }
    
    deinit {
        print("CheckoutViewModel deallocated")
    }
}

Keyboard covering input fields

Symptoms:

  • Keyboard covers the submit button.
  • User can't see what they're typing.
  • Layout doesn't adjust when keyboard appears.

Solution: Proper keyboard avoidance

import SwiftUI

struct CheckoutView: View {
    @StateObject private var viewModel = CheckoutViewModel()
    @FocusState private var focusedField: Field?
    
    enum Field {
        case cardNumber, expiry, cvv
    }
    
    var body: some View {
        ScrollView {
            VStack(spacing: 16) {
                if let dropIn = viewModel.dropIn {
                    dropIn.buildContent()
                }
            }
            .padding()
        }
        .scrollDismissesKeyboard(.interactively)
        .ignoresSafeArea(.keyboard, edges: .bottom)
    }
}

Alternatively, use KeyboardAvoidingView:

struct CheckoutView: View {
    var body: some View {
        GeometryReader { geometry in
            ScrollView {
                VStack {
                    // Your checkout content
                }
                .frame(minHeight: geometry.size.height)
            }
        }
        .ignoresSafeArea(.keyboard)
    }
}

Error codes reference

Error codes to check first

When troubleshooting issues, start by checking for these common error codes:

  • SDK0107: Card is missing in allow funding types
  • SDK0108: PayPal is missing in allow funding types
  • SDK0109: Apple Pay is missing in allow funding types
  • SDK0204: No payment methods available after create(). Fires onError(nil, …) with paymentMethod: nil (initialisation error, not tied to a specific method). An inline noPaymentMethodsError message is still shown in the UI.
  • SDK1100: Failed to retrieve checkout drop-in configuration
  • SDK1116: Card payment failed
  • SDK1117: PayPal payment failed
  • SDK1119: Apple Pay payment failed
  • SDK1120: Invalid PayPal entry type

Get additional help

If you're still experiencing issues, try these troubleshooting steps.

Enable debug logging

Add detailed logging using print statements or OSLog:

import OSLog

let logger = Logger(subsystem: "com.yourapp.checkout", category: "DropIn")

CheckoutDropInConfig(
    // ... other config
    analyticsEvent: { event in
        #if DEBUG
        let encoder = JSONEncoder()
        encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
        
        var eventData: [String: Any] = [
            "eventName": event.eventName,
            "sessionId": event.sessionId,
            "timestamp": event.timestamp
        ]
        
        if let propertiesJson = try? encoder.encode(event),
           let propertiesString = String(data: propertiesJson, encoding: .utf8) {
            logger.debug("Event: \(propertiesString)")
        } else {
            logger.debug("Event: \(event.eventName)")
        }
        #endif
    }
)

Collect diagnostic information

When contacting support, include:

import UIKit

func collectDiagnosticInfo(sessionData: SessionData?, config: CheckoutDropInConfig) -> String {
    var info: [String: Any] = [:]
    
    // Device information
    info["device"] = UIDevice.current.model
    info["systemName"] = UIDevice.current.systemName
    info["systemVersion"] = UIDevice.current.systemVersion
    info["identifierForVendor"] = UIDevice.current.identifierForVendor?.uuidString ?? "N/A"
    
    // Screen information
    let screen = UIScreen.main
    info["screenWidth"] = screen.bounds.width
    info["screenHeight"] = screen.bounds.height
    info["screenScale"] = screen.scale
    
    // App information
    if let bundleInfo = Bundle.main.infoDictionary {
        info["appVersion"] = bundleInfo["CFBundleShortVersionString"] ?? "N/A"
        info["buildNumber"] = bundleInfo["CFBundleVersion"] ?? "N/A"
    }
    
    // SDK information - source from package metadata or SDK constant
    // info["sdkVersion"] = CheckoutDropIn.sdkVersion (if available from SDK)
    info["environment"] = String(describing: config.environment)
    
    // Session info (sanitised - DO NOT log hmacKey, encryptionKey, or API secrets)
    info["sessionIdPresent"] = sessionData?.sessionId != nil
    info["hmacKeyPresent"] = sessionData?.hmacKey != nil
    info["encryptionKeyPresent"] = sessionData?.encryptionKey != nil
    info["allowedFundingTypes"] = String(describing: sessionData?.allowedFundingTypes)
    
    // Timestamp
    info["timestamp"] = ISO8601DateFormatter().string(from: Date())
    
    // Convert to JSON string
    if let jsonData = try? JSONSerialization.data(withJSONObject: info, options: .prettyPrinted),
       let jsonString = String(data: jsonData, encoding: .utf8) {
        print(jsonString)
        
        // Copy to pasteboard
        UIPasteboard.general.string = jsonString
        
        return jsonString
    }
    
    return "Failed to generate diagnostics"
}

When contacting support, always include your merchant ID, environment (test or production), iOS version, device model, and any relevant error messages or console logs. For Paze issues, also note whether wallets.paze is in the session, currency is USD, merchantCategoryCode is present, and the URL callback scheme is registered.