Skip to content

Events

Implement callbacks to customise your Drop-in payment flow.

Overview

Drop-in provides a unified event system that works consistently across all payment methods. You can use these callbacks to inject your own business logic and user experience customisations into the payment flow at critical moments. They ensure that while the SDK handles the complex technical aspects of payment processing, you retain full control over the customer experience and can seamlessly integrate payments into your broader business workflows and systems.

Callbacks enable you to:

  • Validate business rules before payments proceed.
  • Display custom loading states and progress indicators.
  • Show user-friendly error messages.
  • Track analytics and monitor payment performance.
  • Integrate with your own systems for fraud detection or customer management.
  • Implement card-on-file functionality for returning customers.

Your callbacks receive normalised data regardless of whether the customer paid with cards, PayPal, or Google Pay.

All events are optional except onSuccess and onError, which are required for proper payment handling.

Supported events

onGetShopper

This callback provides shopper information for card-on-file functionality, allowing returning customers to use saved payment methods.

You can use it to:

  • Provide shopper ID for logged-in users.
  • Generate anonymous shopper ID for guest checkout.
  • Enable card-on-file for returning customers.
  • Associate payment methods with customer accounts.

Event data

This callback receives no parameters and must return a Shopper object.

Return value

PropertyDescription
id
String
required
A unique shopper identifier. Use customer ID for logged-in users or generate a unique ID for guests.

Example implementation

onGetShopper = {
    // For logged-in users, return their customer ID
    val customerId = getCurrentCustomerId()
    if (customerId != null) {
        return@onGetShopper Shopper(id = customerId)
    }
    
    // For guest users, generate or retrieve a session-based ID
    val guestId = getOrCreateGuestId()
    Shopper(id = guestId)
}

The shopper ID is used to store and retrieve saved payment methods. For guests, generate a unique ID (e.g., UUID) that persists across the session. For registered users, use their customer/account ID from your system.

onBeforeSubmit

This callback is triggered when a payment method is selected and the user is about to submit payment. This is your last chance to validate data or cancel the payment before it's processed.

You can use it to:

  • Perform final validation before payment submission.
  • Check inventory availability.
  • Verify customer account status.
  • Display confirmation dialogs.
  • Track analytics events.
  • Perform fraud checks.

Event data

ParameterDescription
paymentMethod
PaymentMethod
required
The selected payment method (Card, Paypal, or GooglePay).

Return value

  • true or no return: Continue with payment submission.
  • false: Cancel payment submission.
  • Suspend function: Async validation (resolved value determines whether to proceed).

Example implementation

onBeforeSubmit = { paymentMethod ->
    Log.d("CheckoutDropIn", "Payment method selected: $paymentMethod")
    
    // Track analytics
    analytics.track("payment_initiated", mapOf(
        "payment_method" to paymentMethod.toString(),
        "amount" to 25.00
    ))
    
    // Show confirmation for large amounts
    if (amount > 100) {
        val confirmed = showConfirmationDialog(
            "Confirm payment of $amount USD?"
        )
        if (!confirmed) return@onBeforeSubmit false
    }
    
    // Check inventory availability
    val hasStock = checkInventory()
    if (!hasStock) {
        showToast("Some items are no longer available")
        return@onBeforeSubmit false
    }
    
    true // Proceed with payment
}

Simple validation example:

onBeforeSubmit = { paymentMethod ->
    // Simple synchronous validation
    if (!isValidOrder()) {
        showToast("Please complete all required fields")
        return@onBeforeSubmit false // Payment won't proceed
    }
    
    true // Payment will proceed
}

onSubmit

This callback is triggered when payment processing begins, after onBeforeSubmit validation passes. Use this to show loading indicators and disable UI elements.

You can use it to:

  • Show loading spinners or progress indicators.
  • Disable submit buttons to prevent double-submission.
  • Display "Processing payment..." messages.
  • Update UI to show payment is in progress.
  • Track analytics for payment submission.

Event data

ParameterDescription
paymentMethod
PaymentMethod
required
The payment method being processed (Card, Paypal, or GooglePay).

Example implementation

onSubmit = { paymentMethod ->
    Log.d("CheckoutDropIn", "Processing payment with: $paymentMethod")
    
    // Show loading overlay
    showLoadingOverlay(true)
    
    // Disable submit button to prevent double-submission
    disableSubmitButton()
    
    // Track analytics
    analytics.track("payment_processing", mapOf(
        "payment_method" to paymentMethod.toString()
    ))
}

Complete loading state management:

var loadingTimeout: Job? = null

onSubmit = { paymentMethod ->
    // Show loading state
    setLoadingState(true)
    
    // Set timeout in case payment takes too long
    loadingTimeout = lifecycleScope.launch {
        delay(10000) // 10 seconds
        showWarning("Payment is taking longer than expected. Please wait...")
    }
    
    // Track start time for performance monitoring
    paymentStartTime = System.currentTimeMillis()
}

onSuccess

This callback is triggered after payment succeeds. It receives the final transaction result from the payment processing system.

You can use it to:

  • Verify payment on your backend (REQUIRED).
  • Redirect to success page after verification.
  • Display success messages.
  • Track successful payment analytics.
  • Update stock levels for purchased items.
  • Send order confirmation emails to customers.
  • Clear shopping cart.

Event data

Event dataDescription
result
DropInSubmitResult
required
The payment processing result from PXP's backend.
result.systemTransactionId
String
required
Unity's system transaction identifier. Use for backend verification.
result.merchantTransactionId
String?
Your unique transaction identifier (optional).
result.authenticationId
String?
3DS authentication identifier when authentication was performed.
result.paymentMethod
PaymentMethod
required
The payment method used.

Possible values:
  • PaymentMethod.CARD
  • PaymentMethod.PAYPAL
  • PaymentMethod.GOOGLE_PAY
result.paymentData
AuthorisationPaymentData?
Google Pay only: Contains additional payment data including email, shipping address, and shipping option. This is null for card, PayPal, and other non-Google Pay methods.

DropInSubmitResult type:

data class DropInSubmitResult(
    val systemTransactionId: String,
    val merchantTransactionId: String? = null,
    val authenticationId: String? = null,
    val paymentMethod: PaymentMethod,
    val paymentData: AuthorisationPaymentData? = null
)

Google Pay payment data

When a payment is completed using Google Pay, the result.paymentData field contains additional information collected during the Google Pay flow:

data class AuthorisationPaymentData(
    val email: String? = null,
    val shippingAddress: Map<String, String?>? = null,
    val shippingOption: Map<String, String?>? = null
)

These fields are populated based on your Google Pay configuration:

FieldPopulated when
emailemailRequired is true in your Google Pay configuration.
shippingAddressshippingAddressRequired is true in your Google Pay configuration.
shippingOptionGoogle Pay shipping options are configured and the user selects one.

Example handling Google Pay payment data:

onSuccess = { result ->
    Log.d("Checkout", "Payment successful: ${result.systemTransactionId}")
    Log.d("Checkout", "Payment method: ${result.paymentMethod}")
    
    // Check for Google Pay-specific data
    if (result.paymentMethod == PaymentMethod.GOOGLE_PAY && result.paymentData != null) {
        val paymentData = result.paymentData
        
        // Email address if requested
        paymentData.email?.let { email ->
            Log.d("Checkout", "Customer email: $email")
            // Use email for order confirmation
        }
        
        // Shipping address if requested
        paymentData.shippingAddress?.let { address ->
            Log.d("Checkout", "Shipping address: $address")
            // Use address for order fulfillment
        }
        
        // Shipping option if configured
        paymentData.shippingOption?.let { option ->
            Log.d("Checkout", "Shipping option: $option")
            // Use selected shipping method
        }
    }
    
    // CRITICAL: Verify on backend before fulfilling
    verifyPaymentOnBackend(result)
}

The onSuccess callback is a frontend event and can be manipulated by malicious users. Never fulfil orders based solely on this callback. Always verify payments on your backend using Unity webhooks or the Get transaction details API before fulfilling orders.

Example implementation

onSuccess = { result ->
    Log.d("CheckoutDropIn", "Payment successful (frontend notification only)")
    Log.d("CheckoutDropIn", "System transaction ID: ${result.systemTransactionId}")
    Log.d("CheckoutDropIn", "Merchant transaction ID: ${result.merchantTransactionId}")
    Log.d("CheckoutDropIn", "Payment method: ${result.paymentMethod}")
    
    // Clear loading timeout if set
    loadingTimeout?.cancel()
    
    // Hide loading overlay
    setLoadingState(false)
    
    // Track analytics
    val processingTime = System.currentTimeMillis() - paymentStartTime
    analytics.track("payment_success_frontend", mapOf(
        "system_transaction_id" to result.systemTransactionId,
        "merchant_transaction_id" to result.merchantTransactionId,
        "payment_method" to result.paymentMethod.toString(),
        "processing_time" to processingTime
    ))
    
    // Show temporary success message
    showMessage("Payment received! Verifying...", MessageType.SUCCESS)
    
    // CRITICAL: Verify payment on backend before fulfilling order
    lifecycleScope.launch {
        try {
            val verification = verifyPaymentOnBackend(
                systemTransactionId = result.systemTransactionId,
                merchantTransactionId = result.merchantTransactionId
            )
            
            if (verification.success) {
                // Payment verified on backend - safe to proceed
                Log.d("CheckoutDropIn", "Payment verified on backend: ${verification.orderId}")
                
                // Track backend verification success
                analytics.track("payment_verified", mapOf(
                    "order_id" to verification.orderId,
                    "system_transaction_id" to result.systemTransactionId
                ))
                
                // Clear cart
                clearShoppingCart()
                
                // Navigate to success page
                navigateToOrderConfirmation(verification.orderId)
            } else {
                // Verification failed
                Log.e("CheckoutDropIn", "Backend verification failed: ${verification.error}")
                
                analytics.track("payment_verification_failed", mapOf(
                    "system_transaction_id" to result.systemTransactionId,
                    "error" to verification.error
                ))
                
                showError(
                    "Payment verification failed. Please contact support with " +
                    "transaction ID: ${result.merchantTransactionId}"
                )
            }
        } catch (e: Exception) {
            // Network or server error during verification
            Log.e("CheckoutDropIn", "Failed to verify payment", e)
            
            analytics.track("payment_verification_error", mapOf(
                "system_transaction_id" to result.systemTransactionId,
                "error" to e.message
            ))
            
            // Show error but don't clear cart (webhook may still process it)
            showError(
                "Unable to verify payment. Your payment may still be processing. " +
                "Please check your email for confirmation or contact support with " +
                "transaction ID: ${result.merchantTransactionId}"
            )
        }
    }
}

onError

This callback is triggered when an error occurs during the payment process.

You can use it to:

  • Display user-friendly error messages.
  • Log errors for debugging and monitoring.
  • Track failed payment analytics.
  • Offer alternative payment methods.
  • Provide retry options.
  • Hide loading indicators.
  • Re-enable form controls.

Event data

ParameterDescription
error
BaseSdkException
required
The error object containing details about what went wrong.
error.message
String
required
A human-readable error message.
error.code
String
required
SDK error code in format SDK#### (e.g., 'SDK1113' for authentication failed, 'SDK0500' for network error). Use this for programmatic error handling.

BaseSdkException type:

interface BaseSdkException {
    val message: String
    val code: String
}

Common error scenarios

The following table shows common error scenarios and how to detect and respond to them:

ScenarioDetection approachUser action
Card declinederror.message contains "declined" or specific provider messagesTry a different card.
Insufficient fundserror.message contains "insufficient funds"Use a different payment method.
Expired carderror.message contains "expired"Use a different card.
Invalid CVVerror.message contains "CVV" or "security code"Check security code.
3DS authentication failederror.code == "SDK1113" or error.message contains "Authentication failed"Try again or use different card.
3DS timeouterror.message contains "timeout"Check connection and retry.
User cancelled 3DSerror.message contains "cancel"Retry payment.
PayPal errorerror.code == "SDK1116" or payment method is PayPalTry again or use different method.
Session expirederror.message contains "session" or "expired"Refresh page and retry.
Network errorerror.code == "SDK0500"Check connection and retry.
Configuration errorerror.code starts with "SDK01" or "SDK02"Contact support.

Example implementation

onError = { error ->
    Log.e("CheckoutDropIn", "Payment failed")
    Log.e("CheckoutDropIn", "Error code: ${error.code}")
    Log.e("CheckoutDropIn", "Error message: ${error.message}")
    
    // Clear loading timeout if set
    loadingTimeout?.cancel()
    
    // Hide loading overlay
    setLoadingState(false)
    
    // Re-enable submit button
    enableSubmitButton()
    
    // Track error analytics
    analytics.track("payment_failed", mapOf(
        "error_code" to error.code,
        "error_message" to error.message
    ))
    
    // Log error for monitoring
    logErrorToMonitoring(
        category = "payment_error",
        code = error.code,
        message = error.message,
        userAgent = Build.MODEL
    )
    
    // Show user-friendly error message based on error code and message
    val userMessage = when {
        // Check by SDK error code
        error.code == "SDK0500" -> 
            "Network connection issue. Please check your internet connection and try again."
        error.code == "SDK1113" -> 
            "3D Secure authentication failed. Please try again or use a different card."
        error.code == "SDK1115" -> {
            // Card payment failed - check message for specifics
            when {
                error.message.contains("declined", ignoreCase = true) ->
                    "Your card was declined. Please try a different card or contact your bank for more information."
                error.message.contains("insufficient funds", ignoreCase = true) ->
                    "Insufficient funds. Please use a different payment method."
                error.message.contains("expired", ignoreCase = true) ->
                    "This card has expired. Please use a different card."
                error.message.contains("cvv", ignoreCase = true) || 
                error.message.contains("security code", ignoreCase = true) ->
                    "Invalid security code. Please check the CVV on the back of your card and try again."
                else ->
                    "Card payment failed. Please check your details and try again."
            }
        }
        error.code == "SDK1116" ->
            "PayPal payment failed. Please try again or use a different payment method."
        error.code == "SDK1117" ->
            "Google Pay payment failed. Please try again or use a different payment method."
        error.message.contains("timeout", ignoreCase = true) ->
            "Request timed out. Please check your internet connection and try again."
        error.message.contains("session", ignoreCase = true) || 
        error.message.contains("expired", ignoreCase = true) ->
            "Your payment session has expired. Please refresh the page and try again."
        else -> error.message
    }
    
    showErrorMessage(userMessage)
}

Error handling with retry logic:

var retryCount = 0
val MAX_RETRIES = 3

onError = { error ->
    // Check if error is retryable (network issues, timeouts)
    val isNetworkError = error.code == "SDK0500"
    val isTimeout = error.message.contains("timeout", ignoreCase = true)
    val isRetryable = isNetworkError || isTimeout
    
    if (isRetryable && retryCount < MAX_RETRIES) {
        retryCount++
        
        Log.d("CheckoutDropIn", "Retryable error, attempt $retryCount/$MAX_RETRIES")
        
        showMessage(
            "Connection issue. Attempt $retryCount/$MAX_RETRIES. Please try your payment again.",
            MessageType.WARNING
        )
    } else {
        retryCount = 0 // Reset retry count
        
        if (isRetryable) {
            showMessage(
                "Unable to process payment after multiple attempts. " +
                "Please check your connection and try again later.",
                MessageType.ERROR
            )
        } else {
            showMessage(
                "Payment failed: ${error.message}",
                MessageType.ERROR
            )
        }
        
        // Show alternative options for persistent failures
        showAlternativePaymentMethods()
    }
}

onGetConsent

This callback controls whether to show a consent checkbox for a specific payment method. This is typically used to get user permission to save payment information for future use (card-on-file).

You can use it to:

  • Show consent checkbox only for specific payment methods.
  • Comply with regulations requiring explicit consent for storing payment data.
  • Allow users to opt-in to card-on-file functionality.
  • Control consent display based on user type (guest vs registered).

Event data

ParameterDescription
paymentMethod
PaymentMethod
required
The payment method being evaluated.

Possible values:
  • PaymentMethod.CARD
  • PaymentMethod.PAYPAL
  • PaymentMethod.GOOGLE_PAY

Return value

  • true: Show consent checkbox for this payment method.
  • false: Hide consent checkbox for this payment method.

Example implementation

methodConfig = DropInMethodConfig(
    global = DropInGlobalConfig(
        // Control consent checkbox for card-on-file
        onGetConsent = { paymentMethod ->
            // Show consent for cards and PayPal, but not for wallet payments
            paymentMethod == PaymentMethod.CARD || paymentMethod == PaymentMethod.PAYPAL
        }
    )
)

Dynamic consent based on user type:

onGetConsent = { paymentMethod ->
    // Only show consent for logged-in users
    val isLoggedIn = checkUserLoginStatus()
    
    if (!isLoggedIn) {
        return@onGetConsent false // Guest users can't save payment methods
    }
    
    // Show consent for cards only
    paymentMethod == PaymentMethod.CARD
}

onCancel

This callback is triggered when a payment is cancelled by the user. This allows you to track cancellations, update UI, or perform cleanup operations.

You can use it to:

  • Track payment abandonment analytics.
  • Show user-friendly cancellation message.
  • Re-enable form fields or buttons.
  • Clear loading indicators.
  • Offer alternative payment methods.
  • Send abandonment emails for cart recovery.

Event data

ParameterDescription
paymentMethod
PaymentMethod
required
The payment method that was cancelled.
data
Any?
Additional cancellation data (varies by payment method).

This callback is triggered when:

  • User closes PayPal popup without completing payment.
  • User cancels Google Pay payment sheet.
  • User closes 3D Secure authentication window.
  • User explicitly cancels the payment flow.

Example implementation

methodConfig = DropInMethodConfig(
    global = DropInGlobalConfig(
        onCancel = { paymentMethod, data ->
            Log.d("CheckoutDropIn", "Payment cancelled: $paymentMethod")
            
            // Track analytics
            analytics.track("payment_cancelled", mapOf(
                "method" to paymentMethod.toString(),
                "timestamp" to System.currentTimeMillis()
            ))
            
            // Hide loading spinner
            setIsProcessing(false)
            
            // Re-enable checkout button
            setCheckoutButtonDisabled(false)
            
            // Show cancellation message
            showNotification(
                type = NotificationType.INFO,
                message = "$paymentMethod payment was cancelled. " +
                         "Please try again or choose a different payment method."
            )
            
            // Reset payment form
            resetPaymentForm()
        }
    )
)

Callback execution order

Understanding the callback flow helps you implement proper payment handling:

  1. onGetShopper: initial setup

    • Called during Drop-in initialisation.
    • Must return shopper ID for card-on-file functionality.
  2. onBeforeSubmit: validation phase

    • User selects payment method and clicks submit.
    • Return true to proceed, false to cancel.
    • Perform business validation and fraud checks.
  3. onSubmit: processing starts (if onBeforeSubmit returns true)

    • Show loading indicators.
    • Disable form fields.
    • Track analytics.
  4. onSuccess or onError: payment completes

    • onSuccess: payment succeeded (verify on backend before fulfilling).
    • onError: payment failed (show error message and offer alternatives).
  5. onCancel: user cancels (optional)

    • Triggered if user closes payment window/sheet.
    • Update UI and track analytics.

Complete integration example

Here's a complete example showing all callbacks working together:

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.ui.Modifier
import com.pxp.checkout.checkoutdropin.CheckoutDropIn
import com.pxp.checkout.checkoutdropin.types.*
import com.pxp.checkout.models.*
import java.time.Instant
import java.util.UUID
import kotlinx.coroutines.*

var loadingTimeout: Job? = null
var retryCount = 0
const val MAX_RETRIES = 3

suspend fun initializeCheckout(context: Context) {
    // Get session from backend
    val sessionData = fetchSessionFromBackend()
    
    // Initialise Drop-in with all callbacks
    val checkoutDropIn = CheckoutDropIn.initialize(
        context = context,
        config = CheckoutDropInConfig(
            environment = Environment.LIVE,
            session = sessionData,
            ownerType = "MerchantGroup",
            ownerId = "MERCHANT-1",
            transactionData = DropInTransactionData(
                amount = 25.0,
                currency = "USD",
                entryType = EntryType.Ecom,
                intent = DropInTransactionIntentData(
                    card = IntentType.Authorisation,
                    paypalDropInIntent = DropInPayPalIntentType.Authorisation
                ),
                merchant = "MERCHANT-1",
                merchantTransactionId = UUID.randomUUID().toString(),
                merchantTransactionDate = { Instant.now().toString() }
            ),
            kountDisabled = false, // OPTIONAL: Set to true to disable Kount fraud detection
            
            // Required: Get shopper information
            onGetShopper = {
                val customerId = getCurrentCustomerId()
                Shopper(
                    id = customerId ?: "guest-${UUID.randomUUID()}"
                )
            },
            
            // Before payment submission
            onBeforeSubmit = { paymentMethod ->
                Log.d("CheckoutDropIn", "Payment method selected: $paymentMethod")
                
                // Track analytics
                analytics.track("payment_initiated", mapOf(
                    "payment_method" to paymentMethod.toString(),
                    "amount" to 25.00
                ))
                
                // Show confirmation for large amounts
                if (25.00 > 100) {
                    val confirmed = showConfirmation(
                        "Confirm payment of 25.00 USD?"
                    )
                    if (!confirmed) return@CheckoutDropInConfig false
                }
                
                // Validate inventory
                val hasStock = checkInventory()
                if (!hasStock) {
                    showToast("Some items are no longer available")
                    return@CheckoutDropInConfig false
                }
                
                true // Proceed with payment
            },
            
            // Payment processing started
            onSubmit = { paymentMethod ->
                Log.d("CheckoutDropIn", "Processing payment...")
                
                // Show loading state
                showLoadingOverlay(true)
                disableSubmitButton()
                
                // Set timeout warning
                loadingTimeout = lifecycleScope.launch {
                    delay(10000)
                    showMessage("Payment is taking longer than expected...", MessageType.WARNING)
                }
                
                // Track processing start
                paymentStartTime = System.currentTimeMillis()
                
                analytics.track("payment_processing", mapOf(
                    "payment_method" to paymentMethod.toString()
                ))
            },
            
            // Payment succeeded (frontend notification)
            onSuccess = { result ->
                Log.d("CheckoutDropIn", "Payment successful (verifying on backend...)")
                
                // Clear timeout
                loadingTimeout?.cancel()
                
                // Calculate processing time
                val processingTime = System.currentTimeMillis() - paymentStartTime
                Log.d("CheckoutDropIn", "Payment processed in ${processingTime}ms")
                
                // Track frontend success
                analytics.track("payment_success_frontend", mapOf(
                    "system_transaction_id" to result.systemTransactionId,
                    "payment_method" to result.paymentMethod.toString(),
                    "processing_time" to processingTime
                ))
                
                // Show verifying message
                showMessage("Payment received! Verifying...", MessageType.SUCCESS)
                
                // CRITICAL: Verify on backend
                lifecycleScope.launch {
                    try {
                        val verified = verifyPaymentOnBackend(
                            systemTransactionId = result.systemTransactionId,
                            merchantTransactionId = result.merchantTransactionId
                        )
                        
                        if (verified.success) {
                            // Backend verification passed
                            analytics.track("payment_verified", mapOf(
                                "order_id" to verified.orderId
                            ))
                            
                            // Clear cart and redirect
                            clearCart()
                            navigateToSuccess(verified.orderId)
                        } else {
                            throw Exception(verified.error ?: "Verification failed")
                        }
                    } catch (e: Exception) {
                        Log.e("CheckoutDropIn", "Verification error", e)
                        showLoadingOverlay(false)
                        showError(
                            "Payment verification failed. Please contact support with " +
                            "transaction ID: ${result.merchantTransactionId}"
                        )
                    }
                }
            },
            
            // Payment failed
            onError = { error ->
                Log.e("CheckoutDropIn", "Payment failed: ${error.code}")
                Log.e("CheckoutDropIn", "Error message: ${error.message}")
                
                // Clear timeout
                loadingTimeout?.cancel()
                
                // Hide loading state
                showLoadingOverlay(false)
                enableSubmitButton()
                
                // Track error
                analytics.track("payment_failed", mapOf(
                    "error_code" to error.code,
                    "error_message" to error.message
                ))
                
                // Log for monitoring
                logError(error)
                
                // Handle retryable errors
                val isNetworkError = error.code == "SDK0500"
                val isTimeout = error.message.contains("timeout", ignoreCase = true)
                if ((isNetworkError || isTimeout) && retryCount < MAX_RETRIES) {
                    retryCount++
                    showMessage(
                        "Connection issue (attempt $retryCount/$MAX_RETRIES). Please try again.",
                        MessageType.WARNING
                    )
                    return@CheckoutDropInConfig
                }
                
                retryCount = 0 // Reset
                
                // Show user-friendly error
                val userMessage = when {
                    error.code == "SDK1113" ->
                        "3D Secure authentication failed."
                    error.code == "SDK1115" ->
                        "Card payment failed."
                    error.code == "SDK1116" ->
                        "PayPal payment failed."
                    error.message.contains("declined", ignoreCase = true) ->
                        "Card declined. Please try a different card."
                    error.message.contains("insufficient funds", ignoreCase = true) ->
                        "Insufficient funds. Please use a different payment method."
                    error.message.contains("expired", ignoreCase = true) ->
                        "This card has expired."
                    error.message.contains("cvv", ignoreCase = true) ->
                        "Invalid security code."
                    else -> "Payment failed. Please try again."
                }
                
                showErrorMessage(userMessage)
                
                // Offer alternatives for card issues
                if (error.code == "SDK1115" || 
                    error.message.contains("card", ignoreCase = true)) {
                    showAlternativePaymentMethods()
                }
            },
            
            // Method-specific configuration
            methodConfig = DropInMethodConfig(
                global = DropInGlobalConfig(
                    // Control consent checkbox
                    onGetConsent = { paymentMethod ->
                        // Show consent for cards and PayPal
                        paymentMethod == PaymentMethod.CARD || 
                        paymentMethod == PaymentMethod.PAYPAL
                    },
                    
                    // Handle cancellation
                    onCancel = { paymentMethod, data ->
                        Log.d("CheckoutDropIn", "Payment cancelled: $paymentMethod")
                        
                        // Clear timeout
                        loadingTimeout?.cancel()
                        
                        // Hide loading
                        showLoadingOverlay(false)
                        enableSubmitButton()
                        
                        // Track cancellation
                        analytics.track("payment_cancelled", mapOf(
                            "payment_method" to paymentMethod.toString()
                        ))
                        
                        showMessage("Payment was cancelled. Please try again.", MessageType.INFO)
                    }
                )
            )
        )
    )
    
    // Create component
    val component = checkoutDropIn.create()
    
    // Render in Compose
    component.Content(modifier = Modifier.fillMaxWidth())
}