Learn how to diagnose and fix common issues with Checkout Drop-in.
If you're experiencing issues with Drop-in, start with these quick diagnostic checks:
import com.pxp.checkout.checkoutdropin.CheckoutDropIn
import com.pxp.checkout.checkoutdropin.types.CheckoutDropInConfig
import android.util.Log
// Drop-in diagnostic helper
fun diagnoseDropIn(
context: Context,
session: SessionConfig?,
config: CheckoutDropInConfig
) {
Log.d("DropInDiagnostics", "=== Checkout Drop-in diagnostics ===")
// Check SDK import
Log.d("DropInDiagnostics", "SDK imported: ${CheckoutDropIn::class.java.name}")
// Check session data
Log.d("DropInDiagnostics", "Session ID: ${if (session?.sessionId != null) "Present" else "Missing"}")
Log.d("DropInDiagnostics", "HMAC key: ${if (session?.hmacKey != null) "Present" else "Missing"}")
Log.d("DropInDiagnostics", "Allowed funding types: ${session?.allowedFundingTypes}")
// Check environment
Log.d("DropInDiagnostics", "Environment: ${config.environment}")
Log.d("DropInDiagnostics", "Owner ID: ${config.ownerId}")
Log.d("DropInDiagnostics", "Owner type: ${config.ownerType}")
// Check device info
Log.d("DropInDiagnostics", "Android version: ${Build.VERSION.SDK_INT}")
Log.d("DropInDiagnostics", "Device: ${Build.MANUFACTURER} ${Build.MODEL}")
Log.d("DropInDiagnostics", "Screen density: ${context.resources.displayMetrics.density}")
// Check network connectivity
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = connectivityManager.activeNetwork
val capabilities = connectivityManager.getNetworkCapabilities(network)
Log.d("DropInDiagnostics", "Network available: ${capabilities != null}")
Log.d("DropInDiagnostics", "===================================")
}Symptoms:
- The Drop-in component doesn't appear in your Compose UI.
- No payment methods are visible.
- There are no errors in Logcat.
| Cause | Solution |
|---|---|
| Compose recomposition issue. | Ensure session data is loaded before rendering CheckoutDropIn. Use LaunchedEffect to fetch session data and conditional rendering. |
| Session data invalid. | Verify session data includes sessionId, hmacKey, and allowedFundingTypes. Check backend session creation logs. |
| No payment methods enabled. | Check allowedFundingTypes in your session. At least one payment method must be enabled in the Unity Portal. |
| Incorrect configuration. | Verify environment, ownerId, and ownerType are set correctly. |
| ProGuard/R8 stripping SDK classes. | Add ProGuard rules to keep SDK classes (see ProGuard section below). |
Diagnostic steps:
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import android.util.Log
import com.pxp.checkout.checkoutdropin.CheckoutDropIn
import com.pxp.checkout.checkoutdropin.types.CheckoutDropInConfig
import com.pxp.checkout.components.checkoutdropincomponent.CheckoutDropInComponent
@Composable
fun DiagnoseRenderingIssue() {
val context = LocalContext.current
var session by remember { mutableStateOf<SessionConfig?>(null) }
var checkoutDropInComponent by remember { mutableStateOf<CheckoutDropInComponent?>(null) }
var diagnostics by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
diagnostics += "Step 1: Fetching session...\n"
try {
val response = apiClient.post("/api/create-session") {
contentType(ContentType.Application.Json)
}
if (response.status.value == 200) {
val result = response.body<SessionResponse>()
if (result.success && result.data != null) {
session = result.data
diagnostics += "Step 2: Session fetched successfully\n"
diagnostics += "Session ID: ${result.data.sessionId}\n"
} else {
diagnostics += "ERROR: Session fetch failed: ${result.error}\n"
}
} else {
diagnostics += "ERROR: HTTP ${response.status.value}\n"
}
} catch (e: Exception) {
diagnostics += "ERROR: ${e.message}\n"
Log.e("DropIn", "Session fetch error", e)
}
// Step 3: Check session data
session?.let { sessionConfig ->
diagnostics += "Step 3: Validating session data...\n"
if (sessionConfig.sessionId.isNullOrEmpty()) {
diagnostics += "ERROR: Session ID missing\n"
} else {
diagnostics += "Session ID present\n"
}
if (sessionConfig.allowedFundingTypes == null) {
diagnostics += "ERROR: No allowed funding types\n"
} else {
var paymentMethodCount = 0
if (sessionConfig.allowedFundingTypes.cards != null) paymentMethodCount++
if (sessionConfig.allowedFundingTypes.wallets?.paypal != null) paymentMethodCount++
if (sessionConfig.allowedFundingTypes.wallets?.googlePay != null) paymentMethodCount++
if (paymentMethodCount == 0) {
diagnostics += "ERROR: No payment methods enabled\n"
} else {
diagnostics += "$paymentMethodCount payment method(s) available\n"
}
}
}
}
// Initialize Drop-in once session is loaded
LaunchedEffect(session) {
session?.let { sessionConfig ->
val checkoutDropIn = CheckoutDropIn.initialize(
context = context,
config = CheckoutDropInConfig(
environment = Environment.TEST,
session = sessionConfig,
ownerType = "MerchantGroup",
ownerId = "MERCHANT-1",
transactionData = DropInTransactionData(
currency = "GBP",
amount = 1.00,
entryType = EntryType.Ecom,
intent = DropInTransactionIntentData(
card = IntentType.Authorisation
),
merchant = "Demo Store",
merchantTransactionId = "test-${System.currentTimeMillis()}",
merchantTransactionDate = { Instant.now().toString() }
),
onSuccess = { result ->
Log.d("DropIn", "Success: ${result.systemTransactionId}")
},
onError = { error ->
Log.e("DropIn", "Error: ${error.message}", error)
}
)
)
checkoutDropInComponent = checkoutDropIn.create()
}
}
Column {
Text(diagnostics)
checkoutDropInComponent?.Content(modifier = Modifier.fillMaxWidth())
}
}Symptoms:
- A "Session expired" error message is displayed.
- The drop-in loads but payment fails immediately.
- Logcat shows session timeout errors.
| Cause | Solution |
|---|---|
| Session timeout exceeded. | Sessions expire based on the sessionTimeout value (default = 120 minutes). Create a new session if expired. |
| Clock skew | Ensure that the server and device clocks are synchronised. |
| Session reused | Sessions are single-use. Create a new session for each checkout attempt. |
| Invalid HMAC signature | Verify that the HMAC key matches between session creation and SDK initialisation. |
Solution: Implement session refresh
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import kotlinx.coroutines.launch
import com.pxp.checkout.checkoutdropin.CheckoutDropIn
import com.pxp.checkout.checkoutdropin.types.CheckoutDropInConfig
import com.pxp.checkout.components.checkoutdropincomponent.CheckoutDropInComponent
@Composable
fun CheckoutScreenWithSessionRefresh() {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
var session by remember { mutableStateOf<SessionConfig?>(null) }
var checkoutDropInComponent by remember { mutableStateOf<CheckoutDropInComponent?>(null) }
var refreshKey by remember { mutableStateOf(0) }
// Fetch session
LaunchedEffect(refreshKey) {
try {
session = fetchSessionFromBackend()
} catch (e: Exception) {
Log.e("Checkout", "Failed to fetch session", e)
Toast.makeText(
context,
"Failed to load checkout. Please try again.",
Toast.LENGTH_LONG
).show()
}
}
// Initialize Drop-in once session is loaded
LaunchedEffect(session) {
session?.let { sessionConfig ->
val checkoutDropIn = CheckoutDropIn.initialize(
context = context,
config = CheckoutDropInConfig(
environment = Environment.LIVE,
session = sessionConfig,
ownerType = "MerchantGroup",
ownerId = "MERCHANT-1",
transactionData = DropInTransactionData(
currency = "GBP",
amount = 99.99,
entryType = EntryType.Ecom,
intent = DropInTransactionIntentData(
card = IntentType.Authorisation
),
merchant = "Demo Store",
merchantTransactionId = UUID.randomUUID().toString(),
merchantTransactionDate = { Instant.now().toString() }
),
onSuccess = { result ->
coroutineScope.launch {
verifyPaymentOnBackend(result)
}
},
onError = { error ->
// Handle session expiry
if (error.message?.contains("expired", ignoreCase = true) == true ||
error.message?.contains("session", ignoreCase = true) == true) {
Log.w("Checkout", "Session expired, refreshing...")
Toast.makeText(
context,
"Session expired. Refreshing checkout...",
Toast.LENGTH_SHORT
).show()
// Trigger session refresh
refreshKey++
} else {
Log.e("Checkout", "Payment error", error)
Toast.makeText(
context,
"Payment failed: ${error.message}",
Toast.LENGTH_LONG
).show()
}
}
)
)
checkoutDropInComponent = checkoutDropIn.create()
}
}
// Render Drop-in
checkoutDropInComponent?.Content(modifier = Modifier.fillMaxWidth())
}
suspend fun fetchSessionFromBackend(): SessionConfig {
val response = apiClient.post("/api/create-session") {
contentType(ContentType.Application.Json)
}
if (response.status.value != 200) {
throw Exception("Failed to create session: HTTP ${response.status.value}")
}
val result = response.body<SessionResponse>()
if (!result.success || result.data == null) {
throw Exception("Session creation failed: ${result.error}")
}
return result.data
}Symptoms:
- An expected payment method isn't shown.
- Only the card payment method is visible.
- Google Pay doesn't appear even though it's enabled.
| Payment method | Common causes | Solutions |
|---|---|---|
| Cards | Cards aren't enabled in the Unity Portal or are missing from the session configuration. | Enable the Card service in the Unity Portal and verify that the session includes allowedFundingTypes.cards. |
| PayPal | PayPal onboarding wasn't completed or is missing from session configuration. | Complete PayPal onboarding in Unity Portal and verify that the session includes allowedFundingTypes.wallets.paypal. |
| Google Pay | Google Play Services not available, no cards in Google Wallet, or unsupported device. | Verify Google Play Services is installed, add test cards to Google Wallet, and test on a physical device or emulator with Play Services. |
Diagnostic steps:
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
fun diagnosePaymentMethodVisibility(
context: Context,
session: SessionConfig?
) {
Log.d("PaymentMethods", "=== Payment method diagnostics ===")
val fundingTypes = session?.allowedFundingTypes
Log.d("PaymentMethods", "Session funding types: $fundingTypes")
// Check cards
if (fundingTypes?.cards != null) {
Log.d("PaymentMethods", "Cards enabled: ${fundingTypes.cards}")
} else {
Log.w("PaymentMethods", "Cards not enabled in session")
}
// Check PayPal
if (fundingTypes?.wallets?.paypal != null) {
Log.d("PaymentMethods", "PayPal enabled: ${fundingTypes.wallets.paypal}")
} else {
Log.w("PaymentMethods", "PayPal not enabled in session")
}
// Check Google Pay
if (fundingTypes?.wallets?.googlePay != null) {
Log.d("PaymentMethods", "Google Pay configuration present")
// Check Google Play Services availability
val apiAvailability = GoogleApiAvailability.getInstance()
val resultCode = apiAvailability.isGooglePlayServicesAvailable(context)
if (resultCode == ConnectionResult.SUCCESS) {
Log.d("PaymentMethods", "Google Play Services available")
} else {
Log.w("PaymentMethods", "Google Play Services not available: $resultCode")
}
} else {
Log.w("PaymentMethods", "Google Pay not configured in session")
}
// Check device info
Log.d("PaymentMethods", "Android API level: ${Build.VERSION.SDK_INT}")
Log.d("PaymentMethods", "Device: ${Build.MANUFACTURER} ${Build.MODEL}")
Log.d("PaymentMethods", "===================================")
}Symptoms:
- Google Pay button doesn't appear.
- Google Pay sheet doesn't open when tapped.
- Payment fails with Google Pay errors.
| Cause | Solution |
|---|---|
| Google Play Services not installed. | Install Google Play Services on the device or use an emulator with Play Services. Complete Google Pay onboarding in Unity Portal and verify that the session includes allowedFundingTypes.wallets.googlePay. |
| No cards in Google Wallet. | Add test cards to Google Wallet manually. |
| Minimum API level not met. | Google Pay requires Android 7.0 (API level 24) or higher. |
| Merchant ID missing or invalid. | Add a valid Google Pay merchant ID in production (optional for testing). |
| Device not supported. | Test on a physical device or emulator with Google Play. |
Solution:
import com.google.android.gms.wallet.PaymentsClient
import com.google.android.gms.wallet.Wallet
import com.google.android.gms.wallet.IsReadyToPayRequest
import org.json.JSONObject
import org.json.JSONArray
fun checkGooglePayAvailability(context: Context) {
val paymentsClient: PaymentsClient = Wallet.getPaymentsClient(
context,
Wallet.WalletOptions.Builder()
.setEnvironment(WalletConstants.ENVIRONMENT_TEST)
.build()
)
val request = IsReadyToPayRequest.fromJson(
JSONObject().apply {
put("apiVersion", 2)
put("apiVersionMinor", 0)
put("allowedPaymentMethods", JSONArray().apply {
put(JSONObject().apply {
put("type", "CARD")
put("parameters", JSONObject().apply {
put("allowedAuthMethods", JSONArray().apply {
put("PAN_ONLY")
put("CRYPTOGRAM_3DS")
})
put("allowedCardNetworks", JSONArray().apply {
put("VISA")
put("MASTERCARD")
})
})
})
})
}.toString()
)
paymentsClient.isReadyToPay(request).addOnCompleteListener { task ->
if (task.isSuccessful) {
Log.d("GooglePay", "Google Pay is available: ${task.result}")
} else {
Log.w("GooglePay", "Google Pay check failed", task.exception)
}
}
}Symptoms:
- Frontend
onSuccessfires but backend verification fails. - Orders aren't being fulfilled.
- "Payment verification failed" errors.
| Cause | Solution |
|---|---|
| Webhook not configured. | Set up a webhook URL in the Unity Portal and implement a webhook handler on your backend. |
| Webhook authentication failing. | Verify your webhook signature/authentication. Check your HMAC implementation. |
| Race condition (GET before webhook). | Implement a fallback to the Get transaction details API if the webhook hasn't arrived yet. |
| Transaction ID mismatch | Verify that the systemTransactionId and merchantTransactionId match database records. |
Solution: Robust backend verification
// Backend webhook handler (Node.js/Express example)
app.post('/webhooks/pxp', async (req, res) => {
try {
const events = req.body;
// Verify webhook authenticity using HMAC
if (!verifyWebhookSignature(req)) {
console.error('Invalid webhook signature');
return res.status(401).json({ error: 'Unauthorised' });
}
for (const event of events) {
if (event.eventCategory === 'Transaction') {
const txn = event.eventData;
console.log('Processing transaction:', txn.systemTransactionId);
// Idempotency check
const existing = await db.transactions.findOne({
systemTransactionId: txn.systemTransactionId
});
if (existing) {
console.log('Transaction already processed, skipping');
continue;
}
// Verify transaction state
if (txn.state === 'Authorised' || txn.state === 'Captured') {
// Find order by merchant transaction ID
const order = await db.orders.findOne({
transactionId: txn.merchantTransactionId
});
if (!order) {
console.error('Order not found:', txn.merchantTransactionId);
continue;
}
// Verify amount matches
const transactionAmount = txn.amounts?.transactionValue || txn.amount;
if (Math.abs(transactionAmount - order.amount) > 0.01) {
console.error('Amount mismatch:', {
expected: order.amount,
actual: transactionAmount
});
continue;
}
// Mark transaction as processed
await db.transactions.create({
systemTransactionId: txn.systemTransactionId,
merchantTransactionId: txn.merchantTransactionId,
amount: transactionAmount,
state: txn.state,
processedAt: new Date()
});
// Fulfill order
await fulfillOrder(order.id, txn.systemTransactionId);
console.log('Order fulfilled:', order.id);
}
}
}
// Always return success
res.json({ state: 'Success' });
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});Symptoms:
- Drop-in works in debug builds but crashes in release builds.
ClassNotFoundExceptionorNoSuchMethodExceptionin production.
Solution: Add ProGuard rules
Create or update proguard-rules.pro:
# Keep PXP SDK classes
-keep class com.pxp.checkout.** { *; }
-keepclassmembers class com.pxp.checkout.** { *; }
# Keep data classes used for serialization
-keep @kotlinx.serialization.Serializable class ** {
*;
}
# Keep Kotlin metadata
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes InnerClasses
-keepattributes EnclosingMethod
# Keep Compose
-keep class androidx.compose.** { *; }
-keepclassmembers class androidx.compose.** { *; }Symptoms:
- App performance degrades over time.
- Android Studio Profiler shows growing memory usage.
- Out of memory crashes after multiple checkout attempts.
Solution: Proper lifecycle management
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import com.pxp.checkout.checkoutdropin.CheckoutDropIn
import com.pxp.checkout.checkoutdropin.types.CheckoutDropInConfig
import com.pxp.checkout.components.checkoutdropincomponent.CheckoutDropInComponent
@Composable
fun CheckoutScreen() {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
var sessionData by remember { mutableStateOf<SessionConfig?>(null) }
var isActive by remember { mutableStateOf(true) }
// Monitor lifecycle
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_PAUSE -> {
isActive = false
Log.d("Checkout", "Paused")
}
Lifecycle.Event.ON_RESUME -> {
isActive = true
Log.d("Checkout", "Resumed")
}
Lifecycle.Event.ON_DESTROY -> {
isActive = false
sessionData = null
Log.d("Checkout", "Destroyed")
}
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
// Clean up resources
sessionData = null
}
}
// Only render if active
var checkoutDropInComponent by remember { mutableStateOf<CheckoutDropInComponent?>(null) }
LaunchedEffect(isActive, sessionData) {
if (isActive && sessionData != null) {
val checkoutDropIn = CheckoutDropIn.initialize(
context = context,
config = CheckoutDropInConfig(
// ... config
)
)
checkoutDropInComponent = checkoutDropIn.create()
} else {
checkoutDropInComponent = null
}
}
// Render component if available
checkoutDropInComponent?.Content(modifier = Modifier.fillMaxWidth())
}Symptoms:
- Keyboard covers the submit button.
- User can't see what they're typing.
- Layout doesn't adjust when keyboard appears.
Solution: Proper window insets handling
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.pxp.checkout.checkoutdropin.CheckoutDropIn
import com.pxp.checkout.checkoutdropin.types.CheckoutDropInConfig
import com.pxp.checkout.components.checkoutdropincomponent.CheckoutDropInComponent
@Composable
fun CheckoutScreen() {
val context = LocalContext.current
val scrollState = rememberScrollState()
Scaffold(
modifier = Modifier
.fillMaxSize()
.imePadding() // Adjust for keyboard
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(scrollState)
.padding(16.dp)
) {
// Your checkout content
var checkoutDropInComponent by remember { mutableStateOf<CheckoutDropInComponent?>(null) }
LaunchedEffect(Unit) {
val checkoutDropIn = CheckoutDropIn.initialize(
context = context,
config = CheckoutDropInConfig(
// ... config
)
)
checkoutDropInComponent = checkoutDropIn.create()
}
checkoutDropInComponent?.Content(modifier = Modifier.fillMaxWidth())
}
}
}In AndroidManifest.xml:
<activity
android:name=".CheckoutActivity"
android:windowSoftInputMode="adjustResize">
</activity>For a complete list of error codes and their meanings, see the Error handling guide.
If you're still experiencing issues, try these troubleshooting steps.
Add detailed logging using Logcat:
CheckoutDropInConfig(
// ... other config
analyticsEvent = { event ->
if (BuildConfig.DEBUG) {
Log.d("DropInAnalytics", """
Event: ${event.eventName}
Session ID: ${event.sessionId}
Timestamp: ${event.timestamp}
Data: ${event.toMap()}
""".trimIndent())
}
}
)When contacting support, include:
import android.content.Context
import android.os.Build
import org.json.JSONObject
fun collectDiagnosticInfo(context: Context, session: SessionConfig?): String {
val info = JSONObject().apply {
// Device information
put("manufacturer", Build.MANUFACTURER)
put("model", Build.MODEL)
put("androidVersion", Build.VERSION.SDK_INT)
put("androidRelease", Build.VERSION.RELEASE)
// Screen information
val metrics = context.resources.displayMetrics
put("screenWidth", metrics.widthPixels)
put("screenHeight", metrics.heightPixels)
put("screenDensity", metrics.density)
// App information
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
put("appVersion", packageInfo.versionName)
put("appVersionCode", packageInfo.versionCode)
// SDK information (update these placeholders with your actual values)
put("sdkVersion", "1.0.0") // TODO: Replace with actual SDK version from your build
put("environment", "test") // TODO: Replace with actual environment from your config
// Session info (sanitised)
put("sessionIdPresent", session?.sessionId != null)
put("allowedFundingTypes", session?.allowedFundingTypes.toString())
// Timestamp
put("timestamp", Instant.now().toString())
}
val diagnostics = info.toString(2)
Log.d("Diagnostics", diagnostics)
// Copy to clipboard
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Diagnostic Info", diagnostics)
clipboard.setPrimaryClip(clip)
return diagnostics
}When contacting support, always include your merchant ID, environment (test or production), Android version, device model, and any relevant error messages or Logcat logs.