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
    // Required for init: non-empty sessionId and hmacKey (empty → SDK0103 / SDK0104 throws).
    // encryptionKey is needed for card secured fields; empty may not throw at init.
    print("Session ID: \((sessionData?.sessionId.isEmpty == false) ? "Present" : "Missing")")
    print("HMAC key: \((sessionData?.hmacKey.isEmpty == false) ? "Present" : "Missing")")
    print("Encryption key: \((sessionData?.encryptionKey.isEmpty == false) ? "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

Look for these 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 expecting the full Drop-in UI. While create() is running, buildContent() shows a built-in loading indicator (CheckoutDropInInitializingView). If getCheckoutDropInConfig fails or no payment methods can be created, create() finishes without a component and onError fires (for example SDK1100 or SDK0204); buildContent() may then return an empty view. Wire onError so failures are not mistaken for a silent blank screen.
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.Required for init: non-empty sessionId and hmacKey (empty values throw SDK0103 / SDK0104 from CheckoutDropIn(config:)). Also supply a valid encryptionKey from the session response for card tokenisation — an empty encryption key may not throw at init but will break secured fields later. Verify allowedFundingTypes and 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. For cards, Drop-in requires the cards key (an empty array is valid). cardSchemes alone doesn't enable the card panel.
Card panel hidden by display config.If wallets aren't available and DropInCardConfig sets both showCOF and showNewCard to false, Drop-in creates no payment methods and can fire SDK0204. Enable at least one card section, or enable another payment method in the session.
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 = ""
    private var isCreatingDropIn = false
    private var createFailed = false
    
    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: Drop-in requires the cards key (empty array is valid).
                // cardSchemes alone does not enable the card panel.
                if sessionData.allowedFundingTypes?.cards != nil {
                    paymentMethodCount += 1
                    diagnostics += "Cards key present\n"
                } else if sessionData.allowedFundingTypes?.cardSchemes != nil {
                    diagnostics += "WARNING: cardSchemes is set but does not enable the Card panel (cards key missing)\n"
                }
                
                // 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
                }
                
                // Aeropay: funding object present (credentials validated at create)
                if sessionData.allowedFundingTypes?.payByBanks?.aeropay != nil {
                    paymentMethodCount += 1
                }
                
                if paymentMethodCount == 0 {
                    diagnostics += "ERROR: No payment methods enabled in session (SDK0204 risk)\n"
                } else {
                    diagnostics += "\(paymentMethodCount) payment method(s) available from session checks\n"
                    diagnostics += "Note: session checks alone can still produce SDK0204 if card display config hides Card and no wallets render\n"
                }
            }
            
            // Card display overrides (defaults true when omitted)
            let showCOF = true
            let showNewCard = true
            diagnostics += "showCOF=\(showCOF) showNewCard=\(showNewCard)\n"
            if !showCOF && !showNewCard {
                diagnostics += "WARNING: Both showCOF and showNewCard are false — Card panel hidden even if session.cards is present\n"
            }
            
            // Initialise Drop-in
            isCreatingDropIn = true
            createFailed = false
            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_GROUP_1", // Merchant group ID (ownerType is always "MerchantGroup")
                methodConfig: DropInMethodConfig(
                    card: DropInCardConfig(showCOF: showCOF, showNewCard: showNewCard)
                ),
                onSuccess: { result in
                    print("Success: \(result.systemTransactionId)")
                },
                onError: { paymentMethod, error in
                    if isCreatingDropIn {
                        createFailed = true
                    }
                    diagnostics += "onError: \(error.errorCode): \(error.errorMessage)\n"
                    print("Error: \(error.errorMessage)")
                }
            )
            
            let instance = try CheckoutDropIn(config: config)
            // Create-time failures (including SDK1100 and method setup errors) are delivered
            // through onError, not thrown from create().
            await instance.create()
            isCreatingDropIn = false
            if !createFailed {
                dropIn = instance
            } else {
                diagnostics += "create() finished but Drop-in did not mount. See onError above.\n"
            }
            
        } catch {
            diagnostics += "ERROR: \(error.localizedDescription)\n"
            print("Diagnostic error: \(error)")
        }
    }
}

Session expired errors

Look for these 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 errorMessage = viewModel.errorMessage {
                VStack {
                    Text(errorMessage)
                        .foregroundColor(.red)
                    
                    Button("Refresh") {
                        Task {
                            await viewModel.refreshSession()
                        }
                    }
                }
            }
            if let dropIn = viewModel.dropIn {
                dropIn.buildContent()
            } else if viewModel.errorMessage == nil {
                ProgressView("Loading checkout...")
            }
        }
        .task {
            await viewModel.loadDropIn()
        }
    }
}

@MainActor
final class CheckoutViewModel: ObservableObject {
    @Published var dropIn: CheckoutDropIn?
    @Published var errorMessage: String?
    private var isCreatingDropIn = false
    private var createFailed = false
    
    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 {
        isCreatingDropIn = true
        createFailed = false
        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_GROUP_1", // Merchant group ID (ownerType is always "MerchantGroup")
            onSuccess: { result in
                Task {
                    await verifyPaymentOnBackend(result)
                }
            },
            onError: { paymentMethod, error in
                if isCreatingDropIn {
                    createFailed = true
                }
                // 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)"
                    }
                }
            }
        )
        
        // try only covers SDK0103 / SDK0104 (empty sessionId / hmacKey).
        // Create-time failures (including SDK1100 and method setup errors) are delivered
        // through onError, not thrown from create().
        let instance = try CheckoutDropIn(config: config)
        await instance.create()
        isCreatingDropIn = false
        if !createFailed {
            dropIn = instance
        }
    }
}

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

Payment method not appearing

Look for these 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
CardsSession missing the cards key (having only cardSchemes is not enough), or both showCOF and showNewCard are false.Enable the Card service in the Unity Portal and verify that the session includes allowedFundingTypes.cards (an empty array is valid). cardSchemes is used for network resolution but does not substitute for cards. Check methodConfig.card: at least one of showCOF or showNewCard must resolve to true for the card panel to appear. For card-on-file only, also return a non-empty shopper id from onGetShopper. Optional session customerProfileId doesn't replace merchantShopperId or onGetShopper for card-on-file. See Implementation — Customer Profile.
PayPalPayPal onboarding wasn't completed, allowedFundingOptions is missing or empty, or entryType isn't .ecom.Complete PayPal onboarding in the Unity Portal. Verify the session includes allowedFundingTypes.wallets.paypal and a non-empty allowedFundingOptions (or a methodConfig.paypal.fundingSources override). PayPal also requires entryType: .ecom (SDK1120 otherwise).
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. Provision cards in Apple Wallet via Apple Pay sandbox testing, not the manual card-form PANs from the Testing page. Prefer a physical device for end-to-end Wallet testing.
AeropayMissing payByBanks.aeropay (panel hidden with no onError), currency isn't USD, entry type isn't .ecom, missing intent, blank credentials, or invalid non-empty shopper data at load.Enable Aeropay in the Unity Portal and confirm the session response includes allowedFundingTypes.payByBanks.aeropay with externalMerchantId and configurationId. If payByBanks.aeropay is missing, the Aeropay panel is not shown and Drop-in does not fire onError for Aeropay. Use currency = "USD" and entryType: .ecom. Set intent.aeropay. Blank credentials or missing intent typically surface as SDK0113 or SDK0115 on onError (not only SDK1125). Malformed non-empty shopper fields fire SDK1300 and hide the panel; empty fields are OK. See Aeropay.

Diagnostic steps:

import PassKit
import PXPCheckoutSDK

func diagnosePaymentMethodVisibility(
    sessionData: SessionData?,
    config: CheckoutDropInConfig
) async {
    print("=== Payment method diagnostics ===")
    
    let fundingTypes = sessionData?.allowedFundingTypes
    print("Session funding types: \(String(describing: fundingTypes))")
    
    // Check cards — requires the cards key (empty array OK). cardSchemes alone is insufficient.
    if fundingTypes?.cards != nil {
        print("Cards key present: \(String(describing: fundingTypes?.cards))")
        print("cardSchemes (networks only): \(String(describing: fundingTypes?.cardSchemes))")
    } else {
        print("WARNING: Cards not enabled in session (allowedFundingTypes.cards must not be nil)")
        if fundingTypes?.cardSchemes != nil {
            print("WARNING: cardSchemes is set but does not enable the Card panel")
        }
    }
    
    let showCOF = config.methodConfig?.card?.showCOF ?? true
    let showNewCard = config.methodConfig?.card?.showNewCard ?? true
    print("showCOF=\(showCOF) showNewCard=\(showNewCard)")
    if showCOF == false && showNewCard == false {
        print("WARNING: Both showCOF and showNewCard are false — Card panel will be hidden even if session.cards is present")
    }
    
    if showCOF {
        let shopper = await config.onGetShopper?()
        if let shopperId = shopper?.id, !shopperId.isEmpty {
            print("onGetShopper returned non-empty id")
        } else {
            print("WARNING: Card-on-file needs onGetShopper to return TransactionShopper(id:) with a non-empty id")
        }
    }
    
    // Check PayPal - must have allowedFundingOptions present and non-empty
    if let paypalConfig = fundingTypes?.wallets?.paypal {
        print("PayPal wallet present")
        if let allowedFundingOptions = paypalConfig.allowedFundingOptions,
           !allowedFundingOptions.isEmpty {
            print("PayPal enabled with funding options: \(allowedFundingOptions)")
        } else if let fundingSources = config.methodConfig?.paypal?.fundingSources,
                  !fundingSources.isEmpty {
            print("PayPal fundingSources override present: \(fundingSources)")
        } else {
            print("WARNING: PayPal wallet present but allowedFundingOptions is missing/empty (and no fundingSources override)")
        }
        if config.transactionData.entryType != .ecom {
            print("WARNING: PayPal requires entryType .ecom (SDK1120)")
        }
    } else {
        print("WARNING: PayPal not enabled (wallets.paypal missing)")
    }
    
    // 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)")
        
        if PKPaymentAuthorizationController.canMakePayments() {
            print("Device supports Apple Pay")
            
            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 Wallet for accepted networks")
                print("  Provision Apple sandbox Wallet cards — not Drop-in manual-entry PANs")
            }
        } 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 Aeropay - funding object present; credentials must be non-blank
    if let aeropay = fundingTypes?.payByBanks?.aeropay {
        print("Aeropay funding present: \(aeropay)")
        let externalMerchantId = aeropay.externalMerchantId ?? ""
        let configurationId = aeropay.configurationId ?? ""
        if externalMerchantId.isEmpty || configurationId.isEmpty {
            print("WARNING: Aeropay credentials incomplete (externalMerchantId or configurationId). Expect SDK0113 via onError")
        }
        if config.transactionData.currency != "USD" {
            print("WARNING: Aeropay requires USD (SDK0116 if not)")
        }
        if config.transactionData.entryType != .ecom {
            print("WARNING: Aeropay requires entryType .ecom (SDK0114 if not)")
        }
        if config.transactionData.intent.aeropay == nil {
            print("WARNING: Aeropay intent missing (expect SDK0115 via onError)")
        }
    } else {
        print("WARNING: Aeropay not configured in session (payByBanks.aeropay missing)")
    }
    
    print("iOS version: \(UIDevice.current.systemVersion)")
    print("Device: \(UIDevice.current.model)")
    
    print("===================================")
}

Apple Pay not working

Look for these 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.Provision Apple sandbox test credentials in Apple Wallet via Apple Pay sandbox testing. Don't reuse Drop-in manual card-entry PANs from the Testing page.
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 (SDK0602 means Apple Pay isn't supported on the current device or iOS version).

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("  Provision Apple sandbox Wallet cards — not Drop-in manual-entry PANs")
    }
    
    // Check iOS version
    if #available(iOS 14.0, *) {
        print("✓ iOS version compatible (14.0+)")
    } else {
        print("✗ iOS version too old (requires 14.0+)")
    }
    
    print("=============================")
}

Aeropay panel not shown

Look for these symptoms:

  • The Aeropay payment method isn't visible.
  • Other methods appear, but Pay by bank via Aeropay doesn't.

Verify these conditions:

  • Session includes allowedFundingTypes.payByBanks.aeropay with non-blank externalMerchantId and configurationId.
  • transactionData.currency is "USD".
  • transactionData.entryType is .ecom.
  • transactionData.intent.aeropay is set.

If payByBanks.aeropay is missing from the session, the Aeropay panel isn't shown and Drop-in doesn't fire onError for Aeropay. Enable Aeropay in the Unity Portal and confirm the session response includes the funding object.

If onError fires with SDK0114 (entry type), SDK0116 (currency), SDK0113 or SDK0115 (create failures — typically the codes merchants see for blank credentials or missing intent), rare SDK1125 (unexpected non-BaseSdkException render failures), or SDK1300 (shopper data), see Aeropay.

Tips for common Aeropay codes:

  • SDK0114: set entryType: .ecom.
  • SDK0116: set currency to "USD".
  • SDK0113: session Aeropay credentials are blank. Fix externalMerchantId and configurationId on the session response. This is the usual code merchants see for blank credentials (not only SDK1125).
  • SDK0115: set intent.aeropay on DropInTransactionIntentData. This is Aeropay intent missing, not card authorisation (SDK1115).
  • SDK1125: fallback for unexpected Aeropay render failures that aren't a BaseSdkException. Blank credentials or missing intent usually surface as SDK0113 or SDK0115 instead.
  • SDK1300: fix malformed non-empty onGetShopper fields, or provide methodConfig.aeropay.userId. Empty fields and empty phone are OK at load.
  • Missing verified user ID on bank-list load typically surfaces as SDK1303, not SDK1311. See Aeropay — Error codes for mid-flow codes (SDK1301SDK1325).

Backend verification failing

Look for these 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

Look for these 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 errorMessage = viewModel.errorMessage {
                Text(errorMessage)
                    .foregroundColor(.red)
            }
            if let dropIn = viewModel.dropIn {
                dropIn.buildContent()
            } else if viewModel.errorMessage == nil {
                ProgressView("Loading...")
            }
        }
        .task {
            await viewModel.loadDropIn()
        }
        // Single-value onChange compiles with the SDK's iOS 14 deployment floor
        .onChange(of: scenePhase) { phase in
            switch phase {
            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

Look for these 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` requires iOS 16+. On iOS 14–15, omit this modifier
        // and use keyboard frame notifications for insets (see below).
        .modifier(ScrollDismissesKeyboardIfAvailable())
        // Do not use `.ignoresSafeArea(.keyboard)` here — that prevents keyboard insets
        // and can leave fields or the submit area covered.
    }
}

private struct ScrollDismissesKeyboardIfAvailable: ViewModifier {
    func body(content: Content) -> some View {
        if #available(iOS 16.0, *) {
            content.scrollDismissesKeyboard(.interactively)
        } else {
            content
        }
    }
}

If you need more control over keyboard insets (especially on iOS 14–15), observe keyboard frame notifications and apply padding yourself. There's no SwiftUI KeyboardAvoidingView type (that name is from React Native).

Error codes reference

Error codes to check first

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

Funding / initialisation

For Drop-in, missing Card / PayPal / Apple Pay / Aeropay funding in the session usually means the method is omitted with no onError. Diagnose via session allowedFundingTypes (and for Card, methodConfig.card showCOF / showNewCard). Treat SDK0107SDK0109 as Component-level create errors when you use standalone components — not the primary Drop-in “method not appearing” signal. Blank Aeropay credentials or missing intent still surface via onError as SDK0113 / SDK0115.

Check these funding and initialisation codes when onError does fire:

  • SDK0113: Aeropay credentials missing or blank in allow funding types (usually what onError reports when credentials fail create). See Aeropay.
  • SDK0114: Aeropay only supports .ecom entry type.
  • SDK0115: Aeropay intent missing on transaction data. Not the same as SDK1115 (card authorisation failure).
  • SDK0116: Aeropay only supports USD currency.
  • SDK0204: No payment methods available after create(). Fires onError(nil, …) with paymentMethod: nil (initialisation error, not tied to a specific method). During create(), the drop-in component is typically not mounted, so buildContent() often returns an empty view — wire onError so this isn't mistaken for a silent blank screen. Inline noPaymentMethodsError UI appears only when a CheckoutDropInComponent is mounted with zero available methods (a rarer path). This can also occur when the session has no wallets and both showCOF and showNewCard are false.
  • SDK0500: Network error.
  • SDK0602: Apple Pay is not supported on this device or iOS version.
  • SDK1100: Failed to retrieve checkout drop-in configuration.
  • SDK1120: Invalid PayPal entry type (entryType must be .ecom).
  • SDK1125: Unexpected Aeropay render fallback (non-BaseSdkException). Blank credentials or missing intent → usually SDK0113 or SDK0115 on onError, not SDK1125.
  • SDK1300: Invalid Aeropay shopper data at load or during the popup flow.
  • SDK1303: Missing or invalid Aerosync payload (for example bank-list load without a verified user ID). Prefer this over SDK1311 when diagnosing that path. See Aeropay — Error codes.

Payment failures

Check these payment failure codes:

  • SDK1115: Drop-in card authorisation failed (not Aeropay SDK0115).
  • SDK1116: Card payment failed.
  • SDK1117: PayPal payment failed.
  • SDK1119: Apple Pay payment failed.
  • SDK1126: Aeropay payment failed (generic fallback when no API errorCode is present).

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
        // BaseAnalyticsEvent encodes eventName, sessionId, and timestamp only.
        // Subclass-specific properties aren't included unless you cast to the concrete event type.
        let timestamp = ISO8601DateFormatter().string(from: event.timestamp)
        logger.debug("Event: \(event.eventName) sessionId=\(event.sessionId) timestamp=\(timestamp)")
        #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 version — there is no CheckoutDropIn.sdkVersion API.
    // Record the Swift Package resolved version from Xcode (Package Dependencies) or Package.resolved.
    info["environment"] = String(describing: config.environment)
    
    // Session info (sanitised - DO NOT log hmacKey, encryptionKey, or API secrets)
    info["sessionIdPresent"] = sessionData?.sessionId.isEmpty == false
    info["hmacKeyPresent"] = sessionData?.hmacKey.isEmpty == false
    info["encryptionKeyPresent"] = sessionData?.encryptionKey.isEmpty == false
    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 live), iOS version, device model, the Swift Package resolved SDK version, and any relevant error messages or console logs.