Implement callbacks to customise your Drop-in payment flow.
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.
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.
This callback receives no parameters and must return a Shopper object.
| Property | Description |
|---|---|
idString required | A unique shopper identifier. Use customer ID for logged-in users or generate a unique ID for guests. |
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.
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.
| Parameter | Description |
|---|---|
paymentMethodPaymentMethod required | The selected payment method (Card, Paypal, or GooglePay). |
trueor no return: Continue with payment submission.false: Cancel payment submission.- Suspend function: Async validation (resolved value determines whether to proceed).
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
}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.
| Parameter | Description |
|---|---|
paymentMethodPaymentMethod required | The payment method being processed (Card, Paypal, or GooglePay). |
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()
}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 | Description |
|---|---|
resultDropInSubmitResult required | The payment processing result from PXP's backend. |
result.systemTransactionIdString required | Unity's system transaction identifier. Use for backend verification. |
result.merchantTransactionIdString? | Your unique transaction identifier (optional). |
result.authenticationIdString? | 3DS authentication identifier when authentication was performed. |
result.paymentMethodPaymentMethod required | The payment method used. Possible values:
|
result.paymentDataAuthorisationPaymentData? | 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
)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:
| Field | Populated when |
|---|---|
email | emailRequired is true in your Google Pay configuration. |
shippingAddress | shippingAddressRequired is true in your Google Pay configuration. |
shippingOption | Google 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.
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}"
)
}
}
}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.
| Parameter | Description |
|---|---|
errorBaseSdkException required | The error object containing details about what went wrong. |
error.messageString required | A human-readable error message. |
error.codeString 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
}The following table shows common error scenarios and how to detect and respond to them:
| Scenario | Detection approach | User action |
|---|---|---|
| Card declined | error.message contains "declined" or specific provider messages | Try a different card. |
| Insufficient funds | error.message contains "insufficient funds" | Use a different payment method. |
| Expired card | error.message contains "expired" | Use a different card. |
| Invalid CVV | error.message contains "CVV" or "security code" | Check security code. |
| 3DS authentication failed | error.code == "SDK1113" or error.message contains "Authentication failed" | Try again or use different card. |
| 3DS timeout | error.message contains "timeout" | Check connection and retry. |
| User cancelled 3DS | error.message contains "cancel" | Retry payment. |
| PayPal error | error.code == "SDK1116" or payment method is PayPal | Try again or use different method. |
| Session expired | error.message contains "session" or "expired" | Refresh page and retry. |
| Network error | error.code == "SDK0500" | Check connection and retry. |
| Configuration error | error.code starts with "SDK01" or "SDK02" | Contact support. |
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()
}
}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).
| Parameter | Description |
|---|---|
paymentMethodPaymentMethod required | The payment method being evaluated. Possible values:
|
true: Show consent checkbox for this payment method.false: Hide consent checkbox for this payment method.
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
}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.
| Parameter | Description |
|---|---|
paymentMethodPaymentMethod required | The payment method that was cancelled. |
dataAny? | 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.
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()
}
)
)Understanding the callback flow helps you implement proper payment handling:
onGetShopper: initial setup- Called during Drop-in initialisation.
- Must return shopper ID for card-on-file functionality.
onBeforeSubmit: validation phase- User selects payment method and clicks submit.
- Return
trueto proceed,falseto cancel. - Perform business validation and fraud checks.
onSubmit: processing starts (ifonBeforeSubmitreturnstrue)- Show loading indicators.
- Disable form fields.
- Track analytics.
onSuccessoronError: payment completesonSuccess: payment succeeded (verify on backend before fulfilling).onError: payment failed (show error message and offer alternatives).
onCancel: user cancels (optional)- Triggered if user closes payment window/sheet.
- Update UI and track analytics.
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())
}