Skip to content

Troubleshooting

Learn how to diagnose and fix common issues with the card iOS SDK components.

Exception types and error codes

SDK exceptions

The card iOS SDK throws specific exceptions for different error scenarios:

Exception typeError codeDescriptionPrevention
Network failuresSDK0500Network or transport failure before authorisation completes.Check device connectivity and implement retry logic for transient failures.
Validation exceptionsSDK0501SDK-side validation failed on submit.Validate all required fields before submission and implement field-level validation callbacks.
Authentication pre-initiationSDK05023DS pre-initiation failed.Verify provider ID and Portal 3DS configuration. Check onPreInitiateAuthentication implementation.
Authentication rejectedSDK0503Authentication rejected. Please try again or contact your bank.Verify 3DS configuration in Portal and check authentication callbacks.
Challenge flowSDK05053DS challenge flow failed.Verify merchant configuration and challenge window settings in onPreAuthentication.
Authentication configurationSDK0507Authentication configuration invalid.Ensure both onPreInitiateAuthentication and onPreAuthentication are set together, or neither.
Token vault failuresSDK0304Token vault operation failed (retrieve, update, or delete).Verify session data, ownerId, and merchantShopperId alignment.
Checkout initialisationVariousPxpCheckout.initialize or create rejected configuration.Validate CheckoutConfig, SessionData, and component type pairings.
Mismatched 3DS callbacksCustom exceptionOnly one of the 3DS callback pair is set.Set both onPreInitiateAuthentication and onPreAuthentication, or clear both for non-3DS flow.

Error handling best practices

Comprehensive error handler

import PXPCheckoutSDK

var submitConfig = CardSubmitComponentConfig()

submitConfig.onSubmitError = { error in
    // Log error for debugging
    print("Card submit error: \(error.errorCode)\(error.errorMessage)")
    
    // Handle specific error types
    switch error.errorCode {
    case "SDK0500":
        showUserMessage("Network connection issue. Please check your internet and try again.")
        offerRetryOption()
        
    case "SDK0501":
        showUserMessage("Please check all required fields are completed correctly.")
        highlightValidationErrors()
        
    case "SDK0502":
        showUserMessage("Payment system temporarily unavailable. Please try again in a few moments.")
        logCriticalError("3DS pre-initiation failed", error: error)
        
    case "SDK0503":
        showUserMessage("Authentication was rejected. Please try again or contact your bank.")
        logCriticalError("3DS authentication rejected", error: error)
        
    case "SDK0505":
        showUserMessage("Authentication failed. Please try again.")
        trackEvent("3ds_challenge_failed")
        
    case "SDK0507":
        showUserMessage("Payment configuration error. Please contact support.")
        logCriticalError("Authentication configuration invalid", error: error)
        
    case let code where code.hasPrefix("SDK03"):
        showUserMessage("Card tokenisation failed. Please check your card details.")
        logTokenVaultError(error)
        
    default:
        showUserMessage("Payment failed. Please try again or contact support.")
        logUnknownError(error)
    }
}

submitConfig.onPostAuthorisation = { result in
    switch result {
    case let authorised as AuthorisedSubmitResult:
        print("Payment successful: \(authorised.provider.message)")
        navigateToSuccessScreen()
        
    case let refused as RefusedSubmitResult:
        print("Payment declined: \(refused.stateData?.message ?? "Unknown")")
        handlePaymentDecline(refused)
        
    case let failed as FailedSubmitResult:
        print("Payment failed: HTTP \(failed.httpStatusCode ?? 0), \(failed.errorReason)")
        handlePaymentFailure(failed)
        
    default:
        print("Unknown result type")
    }
}

func showUserMessage(_ message: String) {
    DispatchQueue.main.async {
        // Show user-friendly error message in your UI
        let alert = UIAlertController(title: "Payment Error", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        // Present alert to user
    }
}

func logCriticalError(_ context: String, error: BaseSdkException) {
    // Send to error monitoring service
    print("Critical error: \(context) - \(error.errorMessage)")
}

func offerRetryOption() {
    // Show retry button in UI
    print("Offering retry option to user")
}

Troubleshooting common issues

Card components or fields don't appear

The symptoms of this are:

  • A blank area where the field or component should render.
  • create succeeds but nothing visible in the view hierarchy.
  • Wrong field behaviour (e.g., card number field showing in the expiry slot).

Diagnostic steps

// Step 1: Verify component creation
do {
    let cardNumber = try pxpCheckout.create(
        .cardNumber,
        componentConfig: CardNumberComponentConfig(label: "Card number")
    ) as! CardNumberComponent
    
    print("✅ Card number component created successfully")
    print("Component type: \(type(of: cardNumber))")
    
} catch {
    print("❌ Component creation failed: \(error)")
    let nsError = error as NSError
    print("Domain: \(nsError.domain), Code: \(nsError.code)")
    print("User info: \(nsError.userInfo)")
}

// Step 2: Verify component is rendered
struct PaymentView: View {
    var body: some View {
        VStack {
            // Make sure buildContent() is called
            cardNumber.buildContent()
                .onAppear {
                    print("Card number field appeared in view hierarchy")
                }
        }
    }
}

// Step 3: Check component configuration
func validateComponentConfiguration() {
    print("=== Component Configuration Validation ===")
    
    // Verify correct component type and config pairing
    let expectedPairings: [(ComponentType, String)] = [
        (.cardNumber, "CardNumberComponentConfig"),
        (.cardExpiryDate, "CardExpiryDateComponentConfig"),
        (.cardCvc, "CardCvcComponentConfig"),
        (.cardHolderName, "CardHolderNameComponentConfig")
    ]
    
    for (type, configName) in expectedPairings {
        print("\(type)\(configName)")
    }
}

Solutions

// Solution 1: Ensure buildContent() is called
struct CorrectImplementation: View {
    let cardNumber: CardNumberComponent
    let cardExpiry: CardExpiryDateComponent
    let cardCvc: CardCvcComponent
    
    var body: some View {
        VStack(spacing: 16) {
            cardNumber.buildContent()
            cardExpiry.buildContent()
            cardCvc.buildContent()
        }
        .padding()
    }
}

// Solution 2: Use correct component type and config pairing
func createComponentsCorrectly() throws {
    // Correct: .cardNumber with CardNumberComponentConfig
    let cardNumber = try pxpCheckout.create(
        .cardNumber,
        componentConfig: CardNumberComponentConfig(label: "Card number")
    ) as! CardNumberComponent
    
    // Incorrect: .cardNumber with CardExpiryDateComponentConfig
    // This will fail or produce unexpected behaviour
}

// Solution 3: Verify all required components are linked
func linkComponentsToSubmit() {
    var submitConfig = CardSubmitComponentConfig()
    
    // Link all required components
    submitConfig.cardNumberComponent = cardNumber
    submitConfig.cardExpiryDateComponent = cardExpiry
    submitConfig.cardCvcComponent = cardCvc
    
    // Verify linkage
    print("Card number linked: \(submitConfig.cardNumberComponent != nil)")
    print("Card expiry linked: \(submitConfig.cardExpiryDateComponent != nil)")
    print("CVC linked: \(submitConfig.cardCvcComponent != nil)")
}

Checkout initialisation or create throws

The symptoms of this are:

  • PxpCheckout.initialize throws before any UI appears.
  • create throws when building a field or component.
  • Error messages about invalid configuration or session data.

Diagnostic steps

// Step 1: Detailed error logging
func initializeCheckoutWithDiagnostics() {
    do {
        let sessionData = SessionData(
            sessionId: "your-session-id",
            hmacKey: "your-hmac-key",
            encryptionKey: "your-encryption-key"
        )
        
        let transactionData = TransactionData(
            amount: Decimal(string: "99.99")!,
            currency: "USD",
            entryType: .ecom,
            intent: TransactionIntentData(card: .authorisation),
            merchantTransactionId: "order-\(UUID().uuidString)",
            merchantTransactionDate: { Date() }
        )
        
        let checkoutConfig = CheckoutConfig(
            environment: .test,
            session: sessionData,
            transactionData: transactionData,
            merchantShopperId: "shopper-123",
            ownerType: "MerchantGroup",
            ownerId: "your-owner-id"
        )
        
        let pxpCheckout = try PxpCheckout.initialize(config: checkoutConfig)
        print("✅ Checkout initialised successfully")
        
    } catch {
        print("❌ Checkout initialisation failed")
        let nsError = error as NSError
        print("Domain: \(nsError.domain)")
        print("Code: \(nsError.code)")
        print("User info: \(nsError.userInfo)")
        print("Localized description: \(error.localizedDescription)")
    }
}

// Step 2: Validate configuration before initialisation
func validateCheckoutConfiguration(_ config: CheckoutConfig) -> [String] {
    var errors: [String] = []
    
    // Validate session data
    if config.session.sessionId.isEmpty {
        errors.append("Session ID is empty")
    }
    if config.session.hmacKey.isEmpty {
        errors.append("HMAC key is empty")
    }
    if config.session.encryptionKey.isEmpty {
        errors.append("Encryption key is empty")
    }
    
    // Validate transaction data
    if config.transactionData.amount <= 0 {
        errors.append("Transaction amount must be greater than zero")
    }
    if config.transactionData.currency.isEmpty {
        errors.append("Currency code is required")
    }
    
    // Validate merchant data
    if config.merchantShopperId.isEmpty {
        errors.append("Merchant shopper ID is required")
    }
    if config.ownerId.isEmpty {
        errors.append("Owner ID is required")
    }
    
    return errors
}

// Step 3: Test with minimal configuration
func testMinimalConfiguration() {
    let minimalConfig = CheckoutConfig(
        environment: .test,
        session: SessionData(
            sessionId: "test-session",
            hmacKey: "test-hmac-key",
            encryptionKey: "test-encryption-key"
        ),
        transactionData: TransactionData(
            amount: Decimal(string: "1.00")!,
            currency: "USD",
            entryType: .ecom,
            intent: TransactionIntentData(card: .authorisation),
            merchantTransactionId: "test-\(UUID().uuidString)",
            merchantTransactionDate: { Date() }
        ),
        merchantShopperId: "test-shopper",
        ownerType: "MerchantGroup",
        ownerId: "test-owner"
    )
    
    do {
        let pxpCheckout = try PxpCheckout.initialize(config: minimalConfig)
        print("✅ Minimal configuration works")
    } catch {
        print("❌ Even minimal configuration fails: \(error)")
    }
}

Solutions

// Solution 1: Refresh session data from backend
func refreshSessionData() async throws -> SessionData {
    // Fetch fresh session data from your backend
    let response = try await fetchSessionFromBackend()
    
    return SessionData(
        sessionId: response.sessionId,
        hmacKey: response.hmacKey,
        encryptionKey: response.encryptionKey
    )
}

// Solution 2: Validate configuration before initialisation
func initializeWithValidation() async {
    do {
        // Get fresh session data
        let sessionData = try await refreshSessionData()
        
        let checkoutConfig = CheckoutConfig(
            environment: .test,
            session: sessionData,
            transactionData: createTransactionData(),
            merchantShopperId: "shopper-123",
            ownerType: "MerchantGroup",
            ownerId: "your-owner-id"
        )
        
        // Validate before initialising
        let validationErrors = validateCheckoutConfiguration(checkoutConfig)
        guard validationErrors.isEmpty else {
            print("Configuration validation failed: \(validationErrors)")
            return
        }
        
        let pxpCheckout = try PxpCheckout.initialize(config: checkoutConfig)
        print("Checkout initialised successfully")
        
    } catch {
        print("Failed to initialise: \(error)")
        handleInitialisationError(error)
    }
}

// Solution 3: Implement proper error recovery
func handleInitialisationError(_ error: Error) {
    if let nsError = error as NSError {
        switch nsError.code {
        case 401:
            // Unauthorised - refresh session
            Task {
                try? await refreshSessionData()
                // Retry initialisation
            }
        case 403:
            // Forbidden - check bundle ID in Portal
            showConfigurationError("App not authorised. Please check Unity Portal configuration.")
        default:
            showConfigurationError("Initialisation failed: \(error.localizedDescription)")
        }
    }
}

Submit never calls onPostAuthorisation

The symptoms of this are:

  • The customer taps pay but nothing happens.
  • No logs from onPostAuthorisation.
  • Submit appears to hang or do nothing.

Diagnostic steps

// Step 1: Verify all required callbacks are implemented
func verifySubmitCallbacks(_ config: CardSubmitComponentConfig) {
    print("=== Submit Callback Verification ===")
    
    // Check required callbacks
    print("onPreAuthorisation set: \(config.onPreAuthorisation != nil ? "✅" : "❌ REQUIRED")")
    print("onPostAuthorisation set: \(config.onPostAuthorisation != nil ? "✅" : "⚠️  Recommended")")
    print("onSubmitError set: \(config.onSubmitError != nil ? "✅" : "⚠️  Recommended")")
    
    // Check component linkage
    print("cardNumberComponent linked: \(config.cardNumberComponent != nil ? "✅" : "❌")")
    print("cardExpiryDateComponent linked: \(config.cardExpiryDateComponent != nil ? "✅" : "❌")")
    print("cardCvcComponent linked: \(config.cardCvcComponent != nil ? "✅" : "❌")")
}

// Step 2: Add comprehensive logging to all callbacks
var submitConfig = CardSubmitComponentConfig()

submitConfig.onStartSubmit = {
    print("🔵 Submit started")
}

submitConfig.onPreTokenisation = {
    print("🔵 Pre-tokenisation callback")
    return true
}

submitConfig.onPostTokenisation = { result in
    print("🔵 Post-tokenisation: \(result)")
}

submitConfig.onPreAuthorisation = { preAuth in
    print("🔵 Pre-authorisation callback")
    print("Gateway token: \(preAuth?.gatewayTokenId ?? "nil")")
    // This is an async closure - you CAN use await here
    return TransactionInitiationData()
}

submitConfig.onPostAuthorisation = { result in
    print("🔵 Post-authorisation callback")
    print("Result type: \(type(of: result))")
}

submitConfig.onSubmitError = { error in
    print("🔴 Submit error: \(error.errorCode) - \(error.errorMessage)")
}

// Step 3: Verify field validation states
func checkFieldValidation() {
    // Note: getValue() is internal and not accessible
    // Validation state is managed internally by the SDK
    
    print("=== Field Validation States ===")
    print("Submit button will be enabled when all fields are valid")
    print("Use onValidation callback to monitor validation state")
}

Solutions

// Solution 1: Implement required callbacks
var submitConfig = CardSubmitComponentConfig()

// REQUIRED: onPreAuthorisation must be implemented
submitConfig.onPreAuthorisation = { preAuth in
    print("Pre-authorisation data: \(String(describing: preAuth))")
    
    // This is an async closure - you CAN use await here
    // Return transaction initiation data
    return TransactionInitiationData(
        psd2Data: PSD2Data(scaExemption: .lowValue)
    )
}

// REQUIRED: onPostAuthorisation handles final result
submitConfig.onPostAuthorisation = { result in
    switch result {
    case is AuthorisedSubmitResult, is CapturedSubmitResult:
        print("Payment successful")
        navigateToSuccessScreen()
    case let refused as RefusedSubmitResult:
        print("Payment declined: \(refused.stateData?.message ?? "Unknown")")
        showDeclineMessage(refused)
    case let failed as FailedSubmitResult:
        print("Payment failed: \(failed.errorReason)")
        showFailureMessage(failed)
    default:
        print("Unknown result type")
    }
}

// Solution 2: Verify component linkage
func createSubmitWithProperLinkage() throws -> CardSubmitComponent {
    // Create field components first
    let cardNumber = try pxpCheckout.create(
        .cardNumber,
        componentConfig: CardNumberComponentConfig(label: "Card number")
    ) as! CardNumberComponent
    
    let cardExpiry = try pxpCheckout.create(
        .cardExpiryDate,
        componentConfig: CardExpiryDateComponentConfig(label: "Expiry")
    ) as! CardExpiryDateComponent
    
    let cardCvc = try pxpCheckout.create(
        .cardCvc,
        componentConfig: CardCvcComponentConfig(label: "CVC")
    ) as! CardCvcComponent
    
    // Link components to submit
    var submitConfig = CardSubmitComponentConfig()
    submitConfig.cardNumberComponent = cardNumber
    submitConfig.cardExpiryDateComponent = cardExpiry
    submitConfig.cardCvcComponent = cardCvc
    
    // Implement callbacks
    submitConfig.onPreAuthorisation = { _ in
        // This is an async closure - you CAN use await here
        return TransactionInitiationData()
    }
    submitConfig.onPostAuthorisation = { _ in }
    
    // Create submit component
    return try pxpCheckout.create(.cardSubmit, componentConfig: submitConfig) as! CardSubmitComponent
}

// Solution 3: Handle async operations correctly
submitConfig.onPreAuthorisation = { preAuth in
    print("Pre-authorisation callback")
    
    // This is an async closure - you CAN use await here for async operations
    return TransactionInitiationData()
}

submitConfig.onPostAuthorisation = { result in
    // Move heavy work off main thread
    Task {
        await processPaymentResult(result)
    }
    
    // Update UI on main thread
    DispatchQueue.main.async {
        updateUIForResult(result)
    }
}

Validation errors or pay button stays disabled

The symptoms of this are:

  • Submit button stays disabled when disableUntilValidated is used.
  • onSubmitError fires with SDK0501 after tapping pay.
  • Fields appear filled but validation fails.

Diagnostic steps

// Step 1: Add validation callback logging
submitConfig.onValidation = { results in
    print("=== Validation Results ===")
    for (index, result) in results.enumerated() {
        print("Field \(index):")
        print("  Valid: \(result.valid ? "✅" : "❌")")
        
        if let errors = result.errors, !errors.isEmpty {
            print("  Errors:")
            for (code, message) in errors {
                print("    [\(code)] \(message)")
            }
        }
    }
}

// Step 2: Check individual field validation
var cardNumberConfig = CardNumberComponentConfig(label: "Card number")
cardNumberConfig.onValidationPassed = { _ in
    print("✅ Card number validation passed")
}
cardNumberConfig.onValidationFailed = { _ in
    print("❌ Card number validation failed")
}

var cardExpiryConfig = CardExpiryDateComponentConfig(label: "Expiry")
cardExpiryConfig.onValidationPassed = { _ in
    print("✅ Expiry validation passed")
}
cardExpiryConfig.onValidationFailed = { _ in
    print("❌ Expiry validation failed")
}

// Step 3: Manually trigger validation
func testFieldValidation() {
    print("=== Manual Validation Test ===")
    
    // Note: getValue() is internal and not accessible
    // Use onValidation callback to inspect validation results
    print("Monitor validation via onValidation callback")
    print("Enable validationOnChange for real-time validation")
}

Solutions

// Solution 1: Enable real-time validation
var cardNumberConfig = CardNumberComponentConfig(
    label: "Card number",
    validationOnChange: true,  // Enable real-time validation
    onValidationPassed: { _ in
        print("Card number valid")
    },
    onValidationFailed: { _ in
        print("Card number invalid")
    }
)

var cardExpiryConfig = CardExpiryDateComponentConfig(
    label: "Expiry",
    validationOnChange: true
)

var cardCvcConfig = CardCvcComponentConfig(
    label: "CVC",
    validationOnChange: true
)

// Solution 2: Handle validation with disableUntilValidated
var submitConfig = CardSubmitComponentConfig()
submitConfig.disableUntilValidated = true

submitConfig.onValidation = { results in
    let allValid = results.allSatisfy { $0.valid }
    print("All fields valid: \(allValid ? "✅" : "❌")")
    
    if !allValid {
        // Show which fields are invalid
        for (index, result) in results.enumerated() where !result.valid {
            print("Field \(index) is invalid")
            if let errors = result.errors {
                for (code, message) in errors {
                    print("  Error [\(code)]: \(message)")
                }
            }
        }
    }
}

// Solution 3: Include billing address validation when using AVS
if submitConfig.avsRequest == true {
    print("⚠️  AVS enabled - billing address fields must also be valid")
    
    // Ensure billing address components are created and linked
    let billing = try pxpCheckout.create(
        .billingAddress,
        componentConfig: BillingAddressComponentConfig()
    ) as! BillingAddressComponent
    
    submitConfig.billingAddressComponents = BillingAddressComponents(
        billingAddressComponent: billing
    )
}

3DS doesn't start or fails mid-flow

The symptoms of this are:

  • Challenge UI never appears.
  • Flow aborts with SDK0502 to SDK0511 errors.
  • Exception mentioning authentication configuration.

Diagnostic steps

// Step 1: Verify 3DS configuration
func verify3DSConfiguration(_ config: CardSubmitComponentConfig) {
    print("=== 3DS Configuration Verification ===")
    
    let preInitiateSet = config.onPreInitiateAuthentication != nil
    let preAuthSet = config.onPreAuthentication != nil
    
    print("onPreInitiateAuthentication set: \(preInitiateSet ? "✅" : "❌")")
    print("onPreAuthentication set: \(preAuthSet ? "✅" : "❌")")
    
    if preInitiateSet != preAuthSet {
        print("⚠️  WARNING: 3DS callbacks must be set together or not at all")
    }
    
    if preInitiateSet && preAuthSet {
        print("✅ 3DS callbacks properly configured")
    } else if !preInitiateSet && !preAuthSet {
        print("ℹ️  Non-3DS flow (no authentication callbacks set)")
    }
}

// Step 2: Add detailed 3DS logging
var submitConfig = CardSubmitComponentConfig()

submitConfig.onPreInitiateAuthentication = {
    print("🔐 3DS Pre-initiation started")
    
    let authData = PreInitiateIntegratedAuthenticationData(
        providerId: "your_provider_id",
        requestorAuthenticationIndicator: .paymentTransaction,
        timeout: 120
    )
    
    print("Provider ID: \(authData.providerId)")
    print("Timeout: \(authData.timeout)")
    
    // This is an async closure - you CAN use await here
    return authData
}

submitConfig.onPostInitiateAuthentication = { result in
    print("🔐 3DS Pre-initiation completed")
    
    if let failed = result as? FailedAuthenticationResult {
        print("❌ Pre-initiation failed: \(failed.errorReason)")
        print("Error code: \(failed.errorCode)")
    } else {
        print("✅ Pre-initiation successful")
    }
}

submitConfig.onPreAuthentication = {
    print("🔐 3DS Authentication started")
    
    let authData = InitiateIntegratedAuthenticationData(
        merchantCountryNumericCode: "840",
        merchantLegalName: "Your Company Ltd",
        challengeWindowSize: .size5,
        requestorChallengeIndicator: .noPreference,
        timeout: 300
    )
    
    print("Merchant country: \(authData.merchantCountryNumericCode)")
    print("Challenge window: \(authData.challengeWindowSize)")
    
    // This is an async closure - you CAN use await here
    return authData
}

submitConfig.onPostAuthentication = { result in
    print("🔐 3DS Authentication completed")
    
    if result is FailedAuthenticationResult {
        print("❌ Authentication failed")
    } else {
        print("✅ Authentication successful")
    }
}

Solutions

// Solution 1: Verify Unity Portal 3DS configuration
// 1. Log in to Unity Portal
// 2. Go to Merchant setup > Merchant groups
// 3. Select your merchant group
// 4. Click Services tab
// 5. Edit Card service
// 6. Click Configure modules
// 7. Enable "ThreeD secure service"

// Solution 2: Implement complete 3DS callback set
var submitConfig = CardSubmitComponentConfig()

// Both 3DS callbacks must be set together
submitConfig.onPreInitiateAuthentication = {
    // This is an async closure - you CAN use await here
    return PreInitiateIntegratedAuthenticationData(
        providerId: "your_3ds_provider_id",
        requestorAuthenticationIndicator: .paymentTransaction,
        timeout: 120
    )
}

submitConfig.onPreAuthentication = {
    // This is an async closure - you CAN use await here
    return InitiateIntegratedAuthenticationData(
        merchantCountryNumericCode: "840",
        merchantLegalName: "Your Company Ltd",
        challengeWindowSize: .size5,
        requestorChallengeIndicator: .noPreference,
        timeout: 300
    )
}

// Optional but recommended
submitConfig.onPostInitiateAuthentication = { result in
    if let failed = result as? FailedAuthenticationResult {
        print("3DS pre-initiation failed: \(failed.errorReason)")
    }
}

submitConfig.onPostAuthentication = { result in
    if result is FailedAuthenticationResult {
        print("3DS authentication failed")
    }
}

// Required for all flows
submitConfig.onPreAuthorisation = { _ in
    // This is an async closure - you CAN use await here
    return TransactionInitiationData()
}

submitConfig.onPostAuthorisation = { result in
    // Handle final result
}

// Solution 3: Implement conditional 3DS
let transactionAmount: Decimal = 150.00
let requiresStrong3DS = transactionAmount > 100.00

submitConfig.onPreInitiateAuthentication = {
    // Return nil to skip 3DS for low amounts
    guard requiresStrong3DS else {
        print("Skipping 3DS for low-value transaction")
        return nil
    }
    
    // This is an async closure - you CAN use await here
    return PreInitiateIntegratedAuthenticationData(
        providerId: "your_provider_id",
        requestorAuthenticationIndicator: .paymentTransaction,
        timeout: 120
    )
}

// If conditional, still set both callbacks
submitConfig.onPreAuthentication = {
    // This is an async closure - you CAN use await here
    return InitiateIntegratedAuthenticationData(
        merchantCountryNumericCode: "840",
        merchantLegalName: "Your Company Ltd",
        challengeWindowSize: .size5,
        requestorChallengeIndicator: .noPreference,
        timeout: 300
    )
}

Debugging tools and techniques

To get more detailed information about errors, use this debugging setup:

// Enable comprehensive logging
class CardPaymentDebugger {
    static func enableDebugMode(_ config: inout CardSubmitComponentConfig) {
        print("🔧 Card Payment Debug Mode Enabled")
        logEnvironmentInfo()
        
        // Wrap existing callbacks with logging
        let originalOnSubmitError = config.onSubmitError
        config.onSubmitError = { error in
            logError(error)
            originalOnSubmitError?(error)
        }
        
        let originalOnPostAuthorisation = config.onPostAuthorisation
        config.onPostAuthorisation = { result in
            logAuthorisationResult(result)
            originalOnPostAuthorisation?(result)
        }
    }
    
    static func logEnvironmentInfo() {
        print("=== Card Payment Environment ===")
        print("Device Model: \(UIDevice.current.model)")
        print("iOS Version: \(UIDevice.current.systemVersion)")
        print("Bundle ID: \(Bundle.main.bundleIdentifier ?? "Unknown")")
        print("App Version: \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "Unknown")")
        print("Build: \(Bundle.main.infoDictionary?["CFBundleVersion"] ?? "Unknown")")
    }
    
    static func logError(_ error: BaseSdkException) {
        print("=== Payment Error Debug ===")
        print("Error Code: \(error.errorCode)")
        print("Error Message: \(error.errorMessage)")
        
        if let userInfo = (error as NSError).userInfo as? [String: Any] {
            print("Additional info:")
            for (key, value) in userInfo {
                print("  \(key): \(value)")
            }
        }
    }
    
    static func logAuthorisationResult(_ result: SubmitResult) {
        print("=== Authorisation Result Debug ===")
        print("Result type: \(type(of: result))")
        
        switch result {
        case let authorised as AuthorisedSubmitResult:
            print("State: Authorised")
            print("Transaction ID: \(authorised.fundingData.transactionId)")
            print("Provider message: \(authorised.provider.message)")
            
        case let captured as CapturedSubmitResult:
            print("State: Captured")
            print("Transaction ID: \(captured.fundingData.transactionId)")
            print("Provider message: \(captured.provider.message)")
            
        case let refused as RefusedSubmitResult:
            print("State: Refused")
            print("Decline reason: \(refused.stateData?.message ?? "Unknown")")
            print("Decline code: \(refused.stateData?.code ?? "Unknown")")
            if let advice = refused.provider.merchantAdvice {
                print("Merchant advice: \(advice.message)")
            }
            
        case let failed as FailedSubmitResult:
            print("State: Failed")
            print("HTTP Status: \(failed.httpStatusCode ?? 0)")
            print("Error code: \(failed.errorCode ?? "Unknown")")
            print("Error reason: \(failed.errorReason)")
            print("Correlation ID: \(failed.correlationId ?? "None")")
            
        default:
            print("Unknown result type")
        }
    }
    
    static func logFieldValues(
        cardNumber: CardNumberComponent?,
        expiry: CardExpiryDateComponent?,
        cvc: CardCvcComponent?
    ) {
        print("=== Field Values Debug ===")
        // Note: getValue() is internal and not accessible
        print("Use onValidation callback to monitor field states")
        print("Enable validationOnChange for real-time monitoring")
    }
}

// Usage in your implementation
var submitConfig = CardSubmitComponentConfig()
CardPaymentDebugger.enableDebugMode(&submitConfig)

// Before submission
CardPaymentDebugger.logFieldValues(
    cardNumber: cardNumber,
    expiry: cardExpiry,
    cvc: cardCvc
)

Prevention and best practices

Proactive error prevention

// Pre-flight checks before initialising payment
func performPaymentPreflightChecks() async -> Bool {
    let checks: [(String, () async -> Bool, String)] = [
        ("Session validity", {
            return self.validateSessionData()
        }, "Session data is invalid or expired"),
        
        ("Network connectivity", {
            return await self.checkNetworkConnectivity()
        }, "Network connection required"),
        
        ("Component configuration", {
            return self.validateComponentConfiguration()
        }, "Component configuration is invalid"),
        
        ("3DS configuration", {
            return self.validate3DSConfiguration()
        }, "3DS configuration is invalid")
    ]
    
    var allPassed = true
    print("=== Payment Pre-flight Checks ===")
    
    for (name, test, failureMessage) in checks {
        let passed = await test()
        print("\(passed ? "✅" : "❌") \(name): \(passed ? "PASSED" : failureMessage)")
        if !passed {
            allPassed = false
        }
    }
    
    return allPassed
}

private func validateSessionData() -> Bool {
    guard !checkoutConfig.session.sessionId.isEmpty,
          !checkoutConfig.session.hmacKey.isEmpty,
          !checkoutConfig.session.encryptionKey.isEmpty else {
        return false
    }
    return true
}

private func checkNetworkConnectivity() async -> Bool {
    guard let url = URL(string: "https://api.pxp.io/health") else { return false }
    
    do {
        let (_, response) = try await URLSession.shared.data(from: url)
        return (response as? HTTPURLResponse)?.statusCode == 200
    } catch {
        return false
    }
}

private func validateComponentConfiguration() -> Bool {
    // Check all required components are created and linked
    return submitConfig.cardNumberComponent != nil &&
           submitConfig.cardExpiryDateComponent != nil &&
           submitConfig.cardCvcComponent != nil
}

private func validate3DSConfiguration() -> Bool {
    let preInitSet = submitConfig.onPreInitiateAuthentication != nil
    let preAuthSet = submitConfig.onPreAuthentication != nil
    
    // Either both set or neither set
    return preInitSet == preAuthSet
}

// Use before payment
func initiatePayment() async {
    let preflightPassed = await performPaymentPreflightChecks()
    
    if preflightPassed {
        await submitPayment()
    } else {
        showPreflightFailureMessage()
    }
}

Monitoring and alerting

// Error monitoring setup
class PaymentErrorMonitor {
    static func captureError(_ error: Error, context: [String: Any] = [:]) {
        var errorData: [String: Any] = [
            "error_message": error.localizedDescription,
            "error_type": String(describing: type(of: error)),
            "device_model": UIDevice.current.model,
            "ios_version": UIDevice.current.systemVersion,
            "bundle_id": Bundle.main.bundleIdentifier ?? "Unknown",
            "timestamp": ISO8601DateFormatter().string(from: Date())
        ]
        
        // Add context
        errorData.merge(context) { _, new in new }
        
        // Add SDK-specific data
        if let sdkError = error as? BaseSdkException {
            errorData["error_code"] = sdkError.errorCode
            errorData["sdk_error"] = true
        }
        
        // Send to your error tracking service
        print("📊 Error captured: \(errorData)")
        // Example: Crashlytics.record(error: error, userInfo: errorData)
    }
    
    static func trackPaymentEvent(_ eventType: String, data: [String: Any] = [:]) {
        var eventData = data
        eventData["event_type"] = eventType
        eventData["timestamp"] = ISO8601DateFormatter().string(from: Date())
        eventData["component"] = "CardPayment"
        
        print("📈 Event tracked: \(eventData)")
        // Send to your analytics service
    }
}

// Integration with card submit
var submitConfig = CardSubmitComponentConfig()

submitConfig.onSubmitError = { error in
    PaymentErrorMonitor.captureError(error, context: [
        "flow_step": "card_submit",
        "transaction_amount": transactionData.amount,
        "currency": transactionData.currency
    ])
}

submitConfig.onPostAuthorisation = { result in
    let eventType: String
    switch result {
    case is AuthorisedSubmitResult, is CapturedSubmitResult:
        eventType = "payment_success"
    case is RefusedSubmitResult:
        eventType = "payment_declined"
    case is FailedSubmitResult:
        eventType = "payment_failed"
    default:
        eventType = "payment_unknown"
    }
    
    PaymentErrorMonitor.trackPaymentEvent(eventType, data: [
        "result_type": String(describing: type(of: result))
    ])
}