Skip to content

How it works

Learn about how card components work.

Overview

Card components allow you to easily integrate a highly secure and customisable payment solution into your Android application. By offering full control over layout, styling, and field visibility, card components ensure a seamless and branded payment experience, while maintaining top-tier security with PXP's Token Vault service and 3DS service.

The Android SDK provides native Jetpack Compose components that integrate seamlessly with modern Android development practices, offering type-safe configuration, reactive state management, and comprehensive error handling.

Integration steps

A full integration involves the following steps:

  1. Set up the Unity Portal: To use the SDK, you first need to activate the Component and Card services in the Unity Portal. You also need to whitelist any package names that you'll be sending requests from.
  2. Install the SDK: Add the latest version of the Android SDK to your project using Gradle and set it up.
  3. Embed components: Initialise the PXP Checkout SDK, create your components, and render them in your Composable UI.
  4. Handle results: Add proper handling for events and errors. Implement analytics to monitor your customers' payment journeys.
  5. Submit and view transactions: Call the submit() method. The SDK then validates all form data, tokenises the card (if needed), and sends the transaction request to PXP. After PXP processes the payment, the SDK receives the response and triggers an onPostAuthorisation callback that receives a transaction response. This response includes:
    • The transaction state, which tells you whether or not it was successful.
    • The transaction's unique identifier.
    • The provider response, if there's no 3DS challenge.
    • The authentication data, if there is a 3DS challenge.

Android-specific integration features

  • Jetpack Compose Integration: Native composable components that integrate with your existing UI
  • Type safety: Kotlin data classes and sealed classes for configuration and results
  • Lifecycle awareness: Proper Android lifecycle management for payment flows
  • State management: Reactive state handling with Compose state management
  • Error handling: Comprehensive error handling with Android-specific patterns
  • Configuration changes: Automatic state preservation during device rotation and configuration changes
  • Memory management: Efficient component cleanup and resource management
  • Thread safety: All component operations are main-thread safe with background processing for network calls

Component types

There are two types of card components:

  • Pre-built components: For convenience and speed. They offer ready-to-use user interfaces, pre-designed and responsive layouts, and a consistent experience.
  • Standalone components: For maximum flexibility and control. They offer individual field control, custom layouts, and fine-grained validation.

You can think of pre-built components as composable containers that manage multiple standalone components internally, while standalone components are individual building blocks you assemble yourself in your Compose UI.

Use cases

Use pre-built components if:

  • You want a complete payment form with minimal setup.
  • You're happy with pre-designed, responsive payment forms that follow Material Design principles.
  • You want to get up and running quickly, with minimal custom styling.
  • You want a proven user interface that follows payment best practices and Android UI guidelines.

Use standalone components if:

  • You want to position and style each payment field (card number, expiry, CVC, etc.) separately in your own custom Compose UI.
  • You're building a completely custom payment form layout that doesn't fit standard patterns.
  • You need to handle validation for each field independently with custom UI feedback.
  • You're integrating with existing Compose forms or complex UI frameworks.

Available components

ComponentDescription
Billing addressCollect a customer's billing address to use for AVS checks during authorisation.
Card-on-fileAllow customers to select previously tokenized cards for quick and easy payments.
Click-onceAllow customers to pay with just one click, using a tokenised card.
New cardSecurely capture full card details for first-time users.

Android implementation examples

class PaymentActivity : ComponentActivity() {
    private lateinit var pxpCheckout: PxpCheckout
    private lateinit var newCardComponent: NewCardComponent
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        setupPxpCheckout()
        setupNewCardComponent()
        
        setContent {
            PaymentScreen()
        }
    }
    
    private fun setupNewCardComponent() {
        val config = NewCardComponentConfig(
            fields = NewCardComponentConfig.Fields().apply {
                cardNumber = CardNumberComponentConfig(
                    isRequired = true,
                    validateOnChange = true
                )
                expiryDate = CardExpiryDateComponentConfig(
                    isRequired = true
                )
                cvc = CardCvcComponentConfig(
                    isRequired = true
                )
                holderName = CardHolderNameComponentConfig(
                    isRequired = false
                )
            },
            submit = CardSubmitComponentConfig(
                onPostAuthorisation = { result ->
                    handlePaymentResult(result)
                }
            )
        )
        
        newCardComponent = pxpCheckout.createComponent(ComponentType.NEW_CARD, config)
    }
    
    @Composable
    private fun PaymentScreen() {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp)
        ) {
            Text(
                text = "Payment Information",
                style = MaterialTheme.typography.headlineMedium,
                modifier = Modifier.padding(bottom = 16.dp)
            )
            
            // Render the pre-built new card component
            pxpCheckout.buildComponentView(
                component = newCardComponent,
                modifier = Modifier.fillMaxWidth()
            )
        }
    }
}

Supported transaction intents

When you initiate a transaction, you have to provide key information about the transaction method, as well as the amount and currency.

The transaction method is made up of three elements:

  • The entry type, which describes the origin of the transaction and determines the supported payment methods and available features. For Android components, this is always ECOM.
  • The funding type, which describes the method used to fund the transaction. In this case, Card.
  • The intent, which describes the purpose of a transaction, indicating the intended money flow direction. Each intent dictates a specific transaction flow and affects how the transaction is handled by the system.

For card transactions, we support the following intents:

IntentDescription
AUTHORISATIONReserve funds on the customer's payment method.
ESTIMATED_AUTHORISATIONReserve funds on the customer's payment method, based on an estimated amount. This method is particularly useful in environments such as hotels, car rental agencies, and fuel stations, where the final charge may vary based on additional services or usage.
PURCHASECapture funds immediately after authorisation.
PAYOUTSend funds to a recipient.
REFUNDReturn funds to a customer.
VERIFICATIONVerify that a card is legitimate and active, without reserving any funds or completing a purchase. This method is particularly useful in environments such as hotels, car rental agencies, and other scenarios where it's important to validate the card upfront, but the final transaction amount may not be known or processed immediately.

Android transaction configuration

val transactionData = TransactionData(
    amount = 99.99,
    currency = CurrencyType.USD,
    entryType = EntryType.ECOM,
    intent = IntentType.AUTHORISATION, // or other supported intents
    merchantTransactionId = "order-${System.currentTimeMillis()}",
    merchantTransactionDate = { Instant.now().toString() },
    shopper = Shopper(
        email = "customer@example.com",
        firstName = "John",
        lastName = "Doe",
        // Additional shopper data
        phone = "+1234567890",
        dateOfBirth = "1990-01-01"
    )
)

val sdkConfig = PxpSdkConfig(
    environment = Environment.TEST,
    session = SessionConfig(
        sessionId = "session-${System.currentTimeMillis()}",
        sessionData = "additional_session_data"
    ),
    transactionData = transactionData,
    clientId = "your_client_id",
    ownerId = "Unity",
    ownerType = "MerchantGroup",
    merchantShopperId = "shopper-123"
)

Intent-specific configurations

class TransactionIntentManager {
    
    fun createAuthorisationConfig(): TransactionData {
        return TransactionData(
            amount = 99.99,
            currency = CurrencyType.USD,
            entryType = EntryType.ECOM,
            intent = IntentType.AUTHORISATION,
            merchantTransactionId = "auth-${System.currentTimeMillis()}",
            merchantTransactionDate = { Instant.now().toString() },
            // Authorisation-specific data
            expiryTime = Instant.now().plus(24, ChronoUnit.HOURS).toString()
        )
    }
    
    fun createPurchaseConfig(): TransactionData {
        return TransactionData(
            amount = 99.99,
            currency = CurrencyType.USD,
            entryType = EntryType.ECOM,
            intent = IntentType.PURCHASE,
            merchantTransactionId = "purchase-${System.currentTimeMillis()}",
            merchantTransactionDate = { Instant.now().toString() },
            // Purchase-specific data
            deliveryAddress = DeliveryAddress(
                addressLine1 = "123 Main St",
                city = "New York",
                postalCode = "10001",
                countryCode = "US"
            )
        )
    }
    
    fun createVerificationConfig(): TransactionData {
        return TransactionData(
            amount = 0.0, // No amount for verification
            currency = CurrencyType.USD,
            entryType = EntryType.Ecom,
            intent = IntentType.Verification,
            merchantTransactionId = "verify-${System.currentTimeMillis()}",
            merchantTransactionDate = { Instant.now().toString() },
            // Verification-specific data
            verificationPurpose = "account_setup"
        )
    }
    
    fun createRefundConfig(originalTransactionId: String, refundAmount: Double): TransactionData {
        return TransactionData(
            amount = refundAmount,
            currency = CurrencyType.USD,
            entryType = EntryType.Ecom,
            intent = IntentType.Purchase, // Note: Refund is not available in actual SDK
            merchantTransactionId = "refund-${System.currentTimeMillis()}",
            merchantTransactionDate = { Instant.now().toString() },
            // Refund-specific data
            originalTransaction = OriginalTransactionReference(
                transactionId = originalTransactionId,
                merchantTransactionId = "original-transaction-id"
            )
        )
    }
}

// Supporting data classes
data class DeliveryAddress(
    val addressLine1: String,
    val city: String,
    val postalCode: String,
    val countryCode: String
)

data class OriginalTransactionReference(
    val transactionId: String,
    val merchantTransactionId: String
)

// These are sealed classes in the actual SDK, not enums
sealed class IntentType {
    object Authorisation : IntentType()
    object EstimatedAuthorisation : IntentType()
    object Purchase : IntentType()
    object Payout : IntentType()
    object Verification : IntentType()
}

sealed class EntryType {
    object Ecom : EntryType()
    object MOTO : EntryType()
}

sealed class CurrencyType {
    object USD : CurrencyType()
    object EUR : CurrencyType()
    object GBP : CurrencyType()
    object BHD : CurrencyType()
    object JPY : CurrencyType()
}

We support recurring payments for subscription-based services where payments are automatically made on a regular schedule (e.g., monthly memberships, subscription services).

Android-specific features

Component lifecycle management

The Android SDK integrates seamlessly with Android's component lifecycle to ensure proper resource management and state preservation:

class PaymentActivity : ComponentActivity() {
    private lateinit var pxpCheckout: PxpCheckout
    private lateinit var newCardComponent: NewCardComponent
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Initialize SDK - only once per activity lifecycle
        initializePxpSdk()
        
        // Create components - can be recreated on configuration changes
        createComponents()
        
        setContent {
            PaymentScreen()
        }
    }
    
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        // SDK automatically preserves component state
        // You can save additional app-specific state here
        outState.putString("payment_session_id", currentSessionId)
        outState.putSerializable("payment_state", currentPaymentState)
    }
    
    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        // SDK automatically restores component state
        // Restore your app-specific state here
        currentSessionId = savedInstanceState.getString("payment_session_id", "")
        currentPaymentState = savedInstanceState.getSerializable("payment_state") as? PaymentState
    }
    
    override fun onDestroy() {
        super.onDestroy()
        // SDK components are automatically cleaned up
        // Additional cleanup for your app logic here
        clearPaymentState()
    }
    
    override fun onPause() {
        super.onPause()
        // SDK handles background state appropriately
        // Payment flows are paused safely
    }
    
    override fun onResume() {
        super.onResume()
        // SDK resumes any paused payment flows
        // Refresh payment state if needed
        refreshPaymentStatus()
    }
}

Configuration change handling

Components automatically handle configuration changes like device rotation:

@Composable
fun AdaptivePaymentScreen() {
    val configuration = LocalConfiguration.current
    val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
    
    // Component state is preserved automatically during rotation
    if (isLandscape) {
        Row(
            modifier = Modifier.fillMaxSize(),
            horizontalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            PaymentFieldsColumn(modifier = Modifier.weight(1f))
            PaymentSummaryColumn(modifier = Modifier.weight(1f))
        }
    } else {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            PaymentFieldsColumn()
            PaymentSummaryColumn()
        }
    }
}

Memory management

The SDK efficiently manages memory and resources:

class PaymentViewModel : ViewModel() {
    private var pxpCheckout: PxpCheckout? = null
    private val paymentComponents = mutableListOf<Component<*>>()
    
    fun initializePayment(context: Context) {
        // Initialize only if not already done
        if (pxpCheckout == null) {
            pxpCheckout = PxpCheckout.builder()
                .withConfig(sdkConfig)
                .withContext(context)
                .build()
        }
    }
    
    fun createPaymentComponents() {
        // Clean up existing components before creating new ones
        paymentComponents.clear()
        
        // Create new components
        val newCardComponent = pxpCheckout?.createComponent(
            ComponentType.NEW_CARD,
            newCardConfig
        )
        newCardComponent?.let { paymentComponents.add(it) }
    }
    
    override fun onCleared() {
        super.onCleared()
        // ViewModel cleanup - SDK handles component cleanup automatically
        paymentComponents.clear()
        pxpCheckout = null
    }
}

State management

@Composable
fun PaymentScreenWithState() {
    var paymentState by remember { mutableStateOf(PaymentState.IDLE) }
    var errorMessage by remember { mutableStateOf<String?>(null) }
    var cardBrand by remember { mutableStateOf<CardBrand?>(null) }
    
    LaunchedEffect(paymentState) {
        when (paymentState) {
            PaymentState.PROCESSING -> {
                // Show loading indicator
            }
            PaymentState.SUCCESS -> {
                // Navigate to success screen
                delay(2000)
                // Navigate away
            }
            PaymentState.ERROR -> {
                // Show error message
            }
            else -> { /* Handle other states */ }
        }
    }
    
    Column(modifier = Modifier.fillMaxSize()) {
        when (paymentState) {
            PaymentState.PROCESSING -> {
                Box(
                    modifier = Modifier.fillMaxSize(),
                    contentAlignment = Alignment.Center
                ) {
                    Column(horizontalAlignment = Alignment.CenterHorizontally) {
                        CircularProgressIndicator()
                        Text(
                            text = "Processing payment...",
                            modifier = Modifier.padding(top = 16.dp)
                        )
                    }
                }
            }
            else -> {
                // Show payment form
                PaymentForm(
                    onPaymentStateChange = { newState ->
                        paymentState = newState
                    },
                    onErrorChange = { error ->
                        errorMessage = error
                    }
                )
            }
        }
    }
}

enum class PaymentState {
    IDLE, PROCESSING, SUCCESS, ERROR
}

Lifecycle integration

class PaymentActivity : ComponentActivity() {
    private lateinit var pxpCheckout: PxpCheckout
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Initialize SDK
        setupPxpCheckout()
        
        setContent {
            PaymentApp()
        }
    }
    
    override fun onResume() {
        super.onResume()
        // Resume any paused payment flows
        pxpCheckout.resumePaymentFlow()
    }
    
    override fun onPause() {
        super.onPause()
        // Pause payment flows if needed
        pxpCheckout.pausePaymentFlow()
    }
    
    override fun onDestroy() {
        super.onDestroy()
        // Clean up SDK resources
        pxpCheckout.cleanup()
    }
}

Error handling

class PaymentErrorHandler {
    
    @Composable
    fun ErrorDisplay(
        error: PaymentError?,
        onDismiss: () -> Unit,
        onRetry: () -> Unit
    ) {
        error?.let { 
            AlertDialog(
                onDismissRequest = onDismiss,
                title = {
                    Text("Payment Error")
                },
                text = {
                    Text(getErrorMessage(it))
                },
                confirmButton = {
                    TextButton(onClick = onRetry) {
                        Text("Retry")
                    }
                },
                dismissButton = {
                    TextButton(onClick = onDismiss) {
                        Text("Cancel")
                    }
                }
            )
        }
    }
    
    private fun getErrorMessage(error: PaymentError): String {
        return when (error.code) {
            "NETWORK_ERROR" -> "Please check your internet connection and try again."
            "INVALID_CARD" -> "Please check your card details and try again."
            "INSUFFICIENT_FUNDS" -> "This card has insufficient funds."
            "CARD_DECLINED" -> "This card was declined. Please try a different card."
            else -> error.errorReason ?: "An unexpected error occurred."
        }
    }
}