Skip to content

Non-3DS transactions

Integrate a frictionless checkout experience.

Overview

By implementing non-3DS transactions into your payment flow, you benefit from:

  • Streamlined checkout: No additional authentication required, providing faster payment completion.
  • Lower transaction friction: Reduced cart abandonment with seamless payment flow.
  • Faster processing: Immediate payment completion without authentication delays.
  • Better mobile experience: Optimised for mobile checkouts where 3DS challenges can be problematic.

Non-3DS transactions are ideal for low-risk scenarios, trusted customers, or when implementing other fraud prevention measures. However, they may have different liability arrangements and potentially higher processing fees.

Payment flow

The non-3DS flow is made up of six key steps.

Step 1: Submission

The customer taps the submit button or the payment is triggered programmatically. The submitAsync() method in CardSubmitComponent is invoked and validation occurs inside it. If validation passes, the method continues with the payment processing. If it fails, the method exits early and doesn't proceed with the transaction.

Step 2: Card tokenisation

If it's a new card, the SDK sends the card details to the tokenisation service. If it's a saved card, the SDK retrieves the existing token.

Step 3: Evaluation

The SDK evaluates whether 3DS authentication is required. In this flow, 3DS isn't required based on factors such as:

  • The transaction intent being .payout.
  • Your configuration not requiring 3DS authentication (i.e., onPreInitiateAuthentication callback not provided).
  • The transaction falling below risk thresholds.
  • Regulatory exemptions being applicable.

Since 3DS isn't required, the flow skips all authentication steps and proceeds directly to authorisation.

Step 4: Authorisation

This is the final processing step where the SDK sends the authorisation request directly to the payment gateway without any 3DS authentication data. The transaction data is processed using standard card payment protocols.

The authorisation step has two associated callbacks:

  • onPreAuthorisation: Provides transaction data for final review. This is your last chance to modify the transaction before authorisation. Note that in non-3DS flows, no authentication results are included.
  • onPostAuthorisation: Receives the final transaction result from the payment gateway. The transaction is either approved or declined with standard payment processing details.

Step 5: Capture

Depending on your configuration, the transaction may be automatically captured or require manual capture later.

Step 6: Completion

You receive the final authorisation response from the payment gateway. The transaction is either approved or declined and final transaction details are available. Since this is a non-3DS flow, no authentication confirmation data is included in the response.

Non-3DS callback details

Non-3DS transactions use simplified callbacks without authentication complexity:

Core callbacks

  • onPreAuthorisation: ((PreAuthorizationData?) async -> TransactionInitiationData?)?

    • Purpose: Last chance to modify transaction data before authorisation.
    • Parameter: PreAuthorizationData? (optional) containing token identifiers (no 3DS data).
    • Return: TransactionInitiationData with additional metadata, exemption data, or custom parameters.
    • Return nil: To cancel the transaction.
    • Note: This callback is async as it returns data for the SDK to process.
  • onPostAuthorisation: ((BaseSubmitResult) -> Void)?

    • Purpose: Receives the final transaction result from the payment gateway.
    • Parameter: BaseSubmitResult (one of AuthorisedSubmitResult, CapturedSubmitResult, RefusedSubmitResult, FailedSubmitResult).
    • Contains: Transaction status, provider response, transaction reference, processing details.
    • Usage: Handle payment success/failure, navigate to appropriate screens, update order status.
    • Note: This callback is NOT async as it only receives results.

Additional callbacks

  • onPreTokenisation: (() -> Bool)?

    • Purpose: Called before card tokenisation begins.
    • Return: true to proceed with tokenisation, false to abort.
    • Usage: Perform final validation, show loading states.
  • onPostTokenisation: ((CardTokenizationResult) -> Void)?

    • Purpose: Receives tokenisation results.
    • Parameter: CardTokenizationResult (either success with token ID or failure with error).
    • Usage: Handle tokenisation success/failure, store token references.
  • onSubmitError: ((BaseSdkException) -> Void)?

    • Purpose: Handles submission errors.
    • Parameter: BaseSdkException containing error details and codes.
    • Usage: Display error messages, implement retry logic, track failures.

Implementation

Before you start

To use non-3DS payments in your application:

  1. Ensure your merchant account supports non-3DS transactions.
  2. Configure your risk settings appropriately in the Unity Portal.
  3. Consider implementing additional fraud prevention measures.

Step 1: Configure your SDK

To start, set up your CheckoutConfig with the required transaction details.

import PXPCheckoutSDK

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",
    onGetShopper: { async in
        TransactionShopper(
            id: "shopper-123",
            email: "customer@example.com",
            firstName: "John",
            lastName: "Doe"
        )
    }
)

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

Step 2: Create components

Create your card components and submit configuration without 3DS callbacks.

let cardNumber = try pxpCheckout.create(
    .cardNumber,
    componentConfig: CardNumberComponentConfig(
        label: "Card number",
        validationOnChange: true
    )
) as! CardNumberComponent

let cardExpiry = try pxpCheckout.create(
    .cardExpiryDate,
    componentConfig: CardExpiryDateComponentConfig(
        label: "Expiry date",
        validationOnChange: true
    )
) as! CardExpiryDateComponent

let cardCvc = try pxpCheckout.create(
    .cardCvc,
    componentConfig: CardCvcComponentConfig(
        label: "Security code",
        validationOnChange: true
    )
) as! CardCvcComponent

var submitConfig = CardSubmitComponentConfig()
submitConfig.submitText = "Pay £99.99"
submitConfig.cardNumberComponent = cardNumber
submitConfig.cardExpiryDateComponent = cardExpiry
submitConfig.cardCvcComponent = cardCvc

// Note: No 3DS callbacks = non-3DS transaction

// onPreAuthorisation is async as it returns data
submitConfig.onPreAuthorisation = { preAuth async in
    print("Pre-authorisation data: \(String(describing: preAuth))")
    return TransactionInitiationData()
}

submitConfig.onPostAuthorisation = { result in
    switch result {
    case let authorised as AuthorisedSubmitResult:
        print("Payment successful: \(authorised.provider.message)")
        // Navigate to success screen
    case let captured as CapturedSubmitResult:
        print("Payment captured: \(captured.provider.message)")
        // Navigate to success screen
    case let refused as RefusedSubmitResult:
        print("Payment declined: \(refused.stateData?.message ?? "Unknown")")
        // Show error message
    case let failed as FailedSubmitResult:
        print("Payment failed: \(failed.errorReason ?? "Unknown error")")
        // Show error message
    default:
        print("Unknown result type")
    }
}

submitConfig.onSubmitError = { error in
    print("Payment error: \(error.errorMessage)")
    // Show error message
}

let cardSubmit = try pxpCheckout.create(.cardSubmit, componentConfig: submitConfig) as! CardSubmitComponent

Never set only one of onPreInitiateAuthentication and onPreAuthentication. The SDK throws an exception if they are mismatched. For a pure non-3DS path, leave all 3DS callbacks unset on CardSubmitComponentConfig. If you need optional 3DS (for example by amount), configure both hooks and return nil from onPreInitiateAuthentication when you want to skip—see 3DS.

Step 3: Handle common scenarios

Low-value transactions

For low-value transactions where 3DS exemptions apply:

class LowValuePaymentManager {
    private let transactionAmount: Decimal
    
    init(transactionAmount: Decimal) {
        self.transactionAmount = transactionAmount
    }
    
    func createNon3DSConfig() -> CardSubmitComponentConfig {
        var config = CardSubmitComponentConfig()
        
        config.submitText = "Pay \(formatCurrency(transactionAmount))"
        
        config.onPreAuthorisation = { _ async in
            // Add risk screening data for fraud detection
            return TransactionInitiationData(
                psd2Data: PSD2Data(scaExemption: .lowValue),
                riskScreeningData: RiskScreeningData(
                    performRiskScreening: true,
                    excludeDeviceData: false,
                    deviceSessionId: generateDeviceSessionId(),
                    userIp: "192.168.1.100",
                    account: RiskScreeningAccount(
                        id: "user_12345678",
                        creationDateTime: "2024-01-15T10:30:00.000Z"
                    ),
                    items: [
                        RiskScreeningItem(
                            price: transactionAmount,
                            quantity: 1,
                            category: "General"
                        )
                    ],
                    fulfillments: [
                        RiskScreeningFulfillment(
                            type: .shipped,
                            recipientPerson: RiskScreeningRecipientPerson(
                                phoneNumber: "+1234567890"
                            )
                        )
                    ]
                )
            )
        }
        
        config.onPostAuthorisation = { result in
            switch result {
            case is AuthorisedSubmitResult, is CapturedSubmitResult:
                print("Low-value payment successful")
                // Track successful low-value payment
                self.trackPaymentSuccess(amount: self.transactionAmount, type: "low_value_non_3ds")
            case let refused as RefusedSubmitResult:
                print("Low-value payment declined: \(refused.stateData?.message ?? "Unknown")")
            default:
                print("Low-value payment failed")
            }
        }
        
        return config
    }
    
    private func formatCurrency(_ amount: Decimal) -> String {
        return\(amount)"
    }
    
    private func generateDeviceSessionId() -> String {
        return UUID().uuidString.replacingOccurrences(of: "-", with: "")
    }
    
    private func trackPaymentSuccess(amount: Decimal, type: String) {
        // Analytics tracking for successful payments
        print("Payment successful: amount=\(amount), type=\(type)")
    }
}

Trusted customer transactions

For returning customers with established trust:

class TrustedCustomerPaymentManager {
    private let customerId: String
    
    init(customerId: String) {
        self.customerId = customerId
    }
    
    func createTrustedCustomerConfig() -> CardSubmitComponentConfig {
        var config = CardSubmitComponentConfig()
        
        config.submitText = "Pay securely"
        
        config.onPreAuthorisation = { _ async in
            // Add risk screening data with trusted customer indicators
            return TransactionInitiationData(
                psd2Data: PSD2Data(scaExemption: .trustedBeneficiary),
                riskScreeningData: RiskScreeningData(
                    performRiskScreening: true,
                    excludeDeviceData: false,
                    deviceSessionId: generateDeviceSessionId(),
                    userIp: "192.168.1.100",
                    account: RiskScreeningAccount(
                        id: self.customerId,
                        creationDateTime: "2024-01-15T10:30:00.000Z"
                    ),
                    items: [
                        RiskScreeningItem(
                            price: 89.99,
                            quantity: 1,
                            category: "Electronics"
                        )
                    ],
                    fulfillments: [
                        RiskScreeningFulfillment(
                            type: .shipped,
                            shipping: RiskScreeningShipping(
                                shippingMethod: .standard
                            ),
                            recipientPerson: RiskScreeningRecipientPerson(
                                phoneNumber: "+1234567890",
                                email: "customer@example.com"
                            )
                        )
                    ]
                )
            )
        }
        
        config.onPostAuthorisation = { result in
            switch result {
            case is AuthorisedSubmitResult, is CapturedSubmitResult:
                print("Trusted customer payment successful")
                self.updateCustomerTrustScore(customerId: self.customerId, event: "successful_payment")
            case is RefusedSubmitResult:
                print("Trusted customer payment declined")
                self.updateCustomerTrustScore(customerId: self.customerId, event: "declined_payment")
            default:
                print("Unknown payment result")
            }
        }
        
        return config
    }
    
    private func updateCustomerTrustScore(customerId: String, event: String) {
        print("Updating trust score for \(customerId): \(event)")
    }
}

Step 4: Handle errors

Implement comprehensive error handling for non-3DS transactions:

class Non3DSErrorHandler {
    
    func handlePaymentError(_ error: BaseSdkException) {
        switch error.errorCode {
        case "SDK0304":
            showError("Your card was declined. Please try a different payment method.")
        case "SDK0500":
            showError("Network error. Please check your connection and try again.")
        case "SDK0305":
            showError("Invalid card details. Please check and try again.")
        default:
            showError("Payment failed. Please try again or use a different payment method.")
        }
    }
    
    private func showError(_ message: String) {
        // Implementation depends on your UI framework
        print("Error: \(message)")
    }
}

Complete example

The following example shows a complete non-3DS implementation using SwiftUI:

import SwiftUI
import PXPCheckoutSDK

struct Non3DSPaymentView: View {
    @StateObject private var viewModel = Non3DSPaymentViewModel()
    
    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 16) {
                Text("Express checkout")
                    .font(.title)
                    .padding(.bottom, 8)
                
                Text("Fast and secure payment without additional verification")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
                    .padding(.bottom, 16)
                
                // Show error message if any
                if let error = viewModel.errorMessage {
                    HStack {
                        Image(systemName: "exclamationmark.triangle")
                        Text(error)
                    }
                    .padding()
                    .background(Color.red.opacity(0.1))
                    .foregroundColor(.red)
                    .cornerRadius(8)
                }
                
                // Render payment component
                if let newCard = viewModel.newCardComponent {
                    newCard.buildContent()
                }
                
                // Loading indicator
                if viewModel.isLoading {
                    HStack {
                        Spacer()
                        VStack(spacing: 8) {
                            ProgressView()
                            Text("Processing payment...")
                                .font(.caption)
                        }
                        Spacer()
                    }
                    .padding()
                }
            }
            .padding()
        }
        .onAppear {
            viewModel.setupComponents()
        }
    }
}

class Non3DSPaymentViewModel: ObservableObject {
    @Published var isLoading = false
    @Published var errorMessage: String?
    var newCardComponent: NewCardComponent?
    
    private var pxpCheckout: PxpCheckout?
    private let errorHandler = Non3DSErrorHandler()
    
    func setupComponents() {
        do {
            // Setup checkout
            let sessionData = SessionData(
                sessionId: "session-\(UUID().uuidString)",
                hmacKey: "your-hmac-key",
                encryptionKey: "your-encryption-key"
            )
            
            let transactionData = TransactionData(
                amount: 29.99,
                currency: "GBP",
                entryType: .ecom,
                intent: TransactionIntentData(card: .authorisation),
                merchantTransactionId: "order-\(UUID().uuidString)",
                merchantTransactionDate: { Date() }
            )
            
            let checkoutConfig = CheckoutConfig(
                environment: .test,
                session: sessionData,
                transactionData: transactionData,
                merchantShopperId: "shopper-456",
                ownerType: "MerchantGroup",
                ownerId: "your-owner-id",
                onGetShopper: { async in
                    TransactionShopper(
                        id: "shopper-456",
                        email: "customer@example.com",
                        firstName: "Jane",
                        lastName: "Smith"
                    )
                }
            )
            
            pxpCheckout = try PxpCheckout.initialize(config: checkoutConfig)
            
            // Setup submit config without 3DS
            var submitConfig = CardSubmitComponentConfig()
            
            // No 3DS callbacks - transaction will be non-3DS
            // onPreAuthorisation is async as it returns data
            submitConfig.onPreAuthorisation = { _ async in
                return TransactionInitiationData(
                    psd2Data: PSD2Data(scaExemption: .lowValue)
                )
            }
            
            submitConfig.onPostAuthorisation = { [weak self] result in
                DispatchQueue.main.async {
                    self?.isLoading = false
                    
                    switch result {
                    case let authorised as AuthorisedSubmitResult:
                        print("Payment successful: \(authorised.provider.message)")
                        self?.navigateToSuccessScreen()
                    case let captured as CapturedSubmitResult:
                        print("Payment captured: \(captured.provider.message)")
                        self?.navigateToSuccessScreen()
                    case let refused as RefusedSubmitResult:
                        print("Payment declined: \(refused.stateData?.message ?? "Unknown")")
                        self?.handlePaymentDecline(refused)
                    case let failed as FailedSubmitResult:
                        print("Payment failed: \(failed.errorReason ?? "Unknown error")")
                        self?.showError("Payment failed. Please try again.")
                    default:
                        print("Unknown result type")
                        self?.showError("Payment completed with unknown status.")
                    }
                }
            }
            
            submitConfig.onSubmitError = { [weak self] error in
                print("Submit error: \(error.errorMessage)")
                DispatchQueue.main.async {
                    self?.isLoading = false
                    self?.errorHandler.handlePaymentError(error)
                }
            }
            
            submitConfig.onStartSubmit = { [weak self] in
                DispatchQueue.main.async {
                    self?.isLoading = true
                }
            }
            
            // Create new card component
            let newCardConfig = NewCardComponentConfig(submit: submitConfig)
            newCardComponent = try pxpCheckout?.create(.newCard, componentConfig: newCardConfig) as? NewCardComponent
            
        } catch {
            showError("Failed to initialise payment: \(error.localizedDescription)")
        }
    }
    
    private func handlePaymentDecline(_ result: RefusedSubmitResult) {
        let declineReason = result.stateData?.code ?? "Unknown"
        
        let message: String
        switch declineReason {
        case "05":
            message = "Payment declined by your bank"
        case "14":
            message = "Invalid card number"
        case "54":
            message = "Card has expired"
        case "61":
            message = "Amount limit exceeded"
        default:
            message = "Payment was declined"
        }
        
        showError(message)
    }
    
    private func navigateToSuccessScreen() {
        // Navigate to success screen
        print("Payment successful!")
    }
    
    private func showError(_ message: String) {
        errorMessage = message
    }
}

Callback data

This section describes the data received by the different callbacks as part of the non-3DS flow.

onPreAuthorisation

The onPreAuthorisation callback receives pre-authorisation data. For non-3DS transactions, the 3DS-related data will be nil.

Pre-authorisation data

The pre-authorisation data contains token identifiers when available.

PreAuthorizationData(
    gatewayTokenId: "gw_token_abc123def456789",
    schemeTokenId: nil
)
ParameterDescription
gatewayTokenIdThe token identifier from the gateway.
schemeTokenIdThe token identifier from the card scheme (if available).

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

submitConfig.onPreAuthorisation = { preAuth async in
    print("Non-3DS authorisation for token: \(String(describing: preAuth?.gatewayTokenId))")
    
    // Access optional token data if available
    if let tokenId = preAuth?.gatewayTokenId {
        print("Gateway token: \(tokenId)")
        
        // Merchant can use gatewayTokenId to retrieve token details from backend
        // and make authorisation decision
        let transactionDecision = await getAuthorisationDecision(tokenId)
        
        if !transactionDecision {
            // Not proceeding with authorisation
            print("Not proceeding with authorisation")
            return nil
        }
    }
    
    // Add fraud prevention data
    return TransactionInitiationData(
        psd2Data: PSD2Data(scaExemption: .lowValue),
        riskScreeningData: RiskScreeningData(
            performRiskScreening: true,
            excludeDeviceData: false,
            deviceSessionId: generateDeviceSessionId(),
            userIp: "192.168.1.100",
            account: RiskScreeningAccount(
                id: "user_12345678",
                creationDateTime: "2024-01-15T10:30:00.000Z"
            ),
            items: [
                RiskScreeningItem(
                    price: 89.99,
                    quantity: 1,
                    category: "Electronics"
                )
            ],
            fulfillments: [
                RiskScreeningFulfillment(
                    type: .shipped,
                    shipping: RiskScreeningShipping(
                        shippingMethod: .standard
                    ),
                    recipientPerson: RiskScreeningRecipientPerson(
                        phoneNumber: "+1234567890",
                        email: "customer@example.com"
                    )
                )
            ]
        )
    )
}

private func generateDeviceSessionId() -> String {
    return UUID().uuidString.replacingOccurrences(of: "-", with: "")
}

Use the gatewayTokenId with the Get masked card data related to gateway token API to retrieve full token details including card scheme, funding source (credit/debit), masked PAN, and expiry date.

Important: 3DS external data should no longer be provided via the callback. For non-3DS flows, no 3DS data is applicable.

onPostAuthorisation

The onPostAuthorisation callback receives the final transaction result (SubmitResult).

Success

If the transaction was successful, you'll receive either an AuthorisedSubmitResult or a CapturedSubmitResult:

AuthorisedSubmitResult(
    state: "Authorised",
    provider: ProviderResponse(
        code: "00",
        message: "Approved",
        cardVerificationCodeResult: "M",
        addressVerificationServiceResult: "Y"
    ),
    fundingData: FundingData(
        cardVerificationCodeResult: "Matched",
        addressVerificationServiceResult: "Y"
    )
)
ParameterDescription
stateThe final state of the transaction.

Possible values:
  • Authorised
  • Captured
  • Refused
providerDetails about the provider's response including code and message.
provider.codeThe raw result code returned by the provider that processed the transaction.
provider.messageThe raw message associated with the result code from the provider that processed the transaction.
provider.cardVerificationCodeResultThe Card Verification Code (CVC) result returned by the provider.
provider.addressVerificationServiceResultThe Address Verification Service (AVS) result returned by the provider.
fundingDataDetails about the payment method.
fundingData.cardVerificationCodeResultThe Card Verification Code (CVC) result in human-readable format.
fundingData.addressVerificationServiceResultThe Address Verification Service (AVS) result in human-readable format.

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

submitConfig.onPostAuthorisation = { result in
    print("Non-3DS payment result: \(result)")
    
    switch result {
    case let authorised as AuthorisedSubmitResult:
        print("Payment successful!")
        print("Provider response: \(authorised.provider.message)")
        print("Transaction ID: \(authorised.fundingData.transactionId)")
        
        // Check verification results
        let fundingData = authorised.fundingData
        if fundingData.cardVerificationCodeResult == "Matched" {
            print("CVC verification passed")
        }
        
        if fundingData.addressVerificationServiceResult == "Y" {
            print("Address verification passed")
        }
        
        // Store transaction details
        storeTransactionRecord(TransactionRecord(
            amount: Decimal(string: "99.99")!,
            currency: "USD",
            cardType: "card", // FundingDataResult doesn't have a scheme property
            processingType: "non-3ds",
            timestamp: Date().toISOString()
        ))
        
        // Navigate to success screen
        navigateToSuccessScreen()
        
    case let captured as CapturedSubmitResult:
        print("Payment captured successfully!")
        print("Provider response: \(captured.provider.message)")
        navigateToSuccessScreen()
        
    case let refused as RefusedSubmitResult:
        print("Payment declined: \(refused.stateData?.message ?? "Unknown")")
        handlePaymentFailure(refused)
        
    case let failed as FailedSubmitResult:
        print("Payment failed: \(failed.errorReason ?? "Unknown error")")
        showError("Payment failed: \(failed.errorReason ?? "Unknown error")")
        
    default:
        print("Unknown result type")
        showError("Payment completed with unknown status")
    }
}

struct TransactionRecord {
    let amount: Decimal
    let currency: String
    let cardType: String
    let processingType: String
    let timestamp: String
}

private func navigateToSuccessScreen() {
    // Navigate to success screen
    print("Navigating to success screen")
}

Failure (Declined)

If the bank or issuer declines the transaction, you'll receive a RefusedSubmitResult:

RefusedSubmitResult(
    state: "Refused",
    stateData: StateData(
        code: "05",
        message: "Do not honour"
    ),
    provider: ProviderResponse(
        code: "05",
        message: "Do not honour",
        merchantAdvice: MerchantAdvice(
            code: "01",
            message: "Try another payment method"
        ),
        cardVerificationCodeResult: "M",
        addressVerificationServiceResult: "Y"
    ),
    fundingData: FundingData(
        cardVerificationCodeResult: "Matched",
        addressVerificationServiceResult: "Y"
    )
)
ParameterDescription
stateThe final state of the transaction. Value: Refused.
stateDataDetails about the refusal including code and message.
stateData.codeThe state code.
stateData.messageThe state message.
providerProvider response with merchant advice (if available).
provider.merchantAdviceGuidance for handling the decline.

Here's an example of how to handle failures:

private func handlePaymentFailure(_ result: RefusedSubmitResult) {
    print("Payment declined: \(result.stateData?.message ?? "Unknown")")
    
    // Check for merchant advice
    if let advice = result.provider.merchantAdvice {
        switch advice.code {
        case "01":
            // Try another payment method
            showError("Payment declined. Please try a different card.")
        case "02":
            // Retry with different amount
            showError("Transaction amount issue. Please contact support.")
        case "03":
            // Contact issuer
            showError("Please contact your bank to authorise this payment.")
        default:
            showError("Payment declined: \(advice.message)")
        }
    } else {
        // Generic decline message based on state code
        let declineReason: String
        switch result.stateData?.code {
        case "05":
            declineReason = "Payment declined by your bank"
        case "14":
            declineReason = "Invalid card number"
        case "54":
            declineReason = "Card has expired"
        case "61":
            declineReason = "Amount limit exceeded"
        default:
            declineReason = "Payment was declined"
        }
        showError(declineReason)
    }
}

private func showError(_ message: String) {
    print("Error: \(message)")
    // Show error to user
}

onSubmitError

If an error occurs during the payment processing, you'll receive error details through the onSubmitError callback.

submitConfig.onSubmitError = { error in
    print("Submit error: \(error.errorMessage)")
    
    switch error.errorCode {
    // Validation Errors
    case "VALIDATION_FAILED":
        showError("Please check your payment details and try again.")
    case "INVALID_CARD_NUMBER":
        showError("Invalid card number. Please check and try again.")
    case "INVALID_EXPIRY_DATE":
        showError("Invalid expiry date. Please check and try again.")
    case "INVALID_CVC":
        showError("Invalid security code. Please check and try again.")
        
    // Processing Errors
    case "TOKENIZATION_FAILED":
        showError("Unable to process card details. Please try again.")
    case "SDK0500":
        showError("Network connection issue. Please check your internet and try again.")
    case "GATEWAY_TIMEOUT":
        showError("Payment system is busy. Please try again in a moment.")
    case "SERVICE_UNAVAILABLE":
        showError("Payment service temporarily unavailable. Please try again later.")
        
    // Card Errors
    case "SDK0304":
        showError("Card tokenisation failed. Please check your card details.")
    case "SDK0305":
        showError("Your card has expired. Please use a different card.")
        
    default:
        showError("Payment failed. Please try again or use a different payment method.")
    }
}

For more information about error handling, see Troubleshooting.