androidengineers.Book a session

Designing an E-commerce App

Payments & PSP Integration

article20 minHard

Payment integration on Android requires security, reliability, and handling complex failure scenarios. This guide covers integrating a Payment Service Provider (PSP) like Stripe, Google Pay, and ensuring PCI compliance on mobile.

PCI Compliance: Key Rule

Never handle raw card numbers on your app or server. The PSP's SDK tokenizes the card number client-side and sends you an opaque token. Your server charges against that token, never seeing the raw PAN (Primary Account Number).

Stripe Integration

// implementation("com.stripe:stripe-android:20.37.5")

class PaymentManager(private val context: Context) {
    private val paymentSheet: PaymentSheet

    init {
        PaymentConfiguration.init(context, BuildConfig.STRIPE_PUBLISHABLE_KEY)
        paymentSheet = PaymentSheet(activity, ::onPaymentResult)
    }

    // Step 1: Get client secret from your server (never from client)
    suspend fun initiatePayment(cartTotal: Int): String {
        // Your server creates a PaymentIntent and returns the client secret
        return serverApi.createPaymentIntent(amount = cartTotal, currency = "usd").clientSecret
    }

    // Step 2: Present Stripe's hosted payment sheet
    fun presentPaymentSheet(clientSecret: String) {
        val configuration = PaymentSheet.Configuration(
            merchantDisplayName = "My Shop",
            allowsDelayedPaymentMethods = false,
            googlePay = PaymentSheet.GooglePayConfiguration(
                environment = PaymentSheet.GooglePayConfiguration.Environment.Production,
                countryCode = "US",
                currencyCode = "USD"
            )
        )
        paymentSheet.presentWithPaymentIntent(clientSecret, configuration)
    }

    // Step 3: Handle result
    private fun onPaymentResult(result: PaymentSheetResult) {
        when (result) {
            is PaymentSheetResult.Completed -> onPaymentSuccess()
            is PaymentSheetResult.Canceled  -> onPaymentCanceled()
            is PaymentSheetResult.Failed    -> onPaymentFailed(result.error)
        }
    }
}

Google Pay Integration

Google Pay uses tokenized card data from the user's Google Wallet:

class GooglePayManager(private val activity: Activity) {
    private val paymentsClient: PaymentsClient = Wallet.getPaymentsClient(
        activity,
        Wallet.WalletOptions.Builder()
            .setEnvironment(WalletConstants.ENVIRONMENT_PRODUCTION)
            .build()
    )

    // Check if Google Pay is available
    suspend fun isGooglePayAvailable(): Boolean {
        val request = IsReadyToPayRequest.fromJson(buildIsReadyToPayJson())
        return paymentsClient.isReadyToPay(request).await()
    }

    // Launch Google Pay sheet
    fun launchGooglePay(priceInCents: Long) {
        val request = PaymentDataRequest.fromJson(buildPaymentDataRequestJson(priceInCents))
        // Launch via AutoResolveHelper or Activity Result API
        AutoResolveHelper.resolveTask(
            paymentsClient.loadPaymentData(request),
            activity,
            GOOGLE_PAY_REQUEST_CODE
        )
    }

    // Handle result in onActivityResult
    fun handleGooglePayResult(resultCode: Int, data: Intent?): GooglePayResult {
        return when (resultCode) {
            Activity.RESULT_OK -> {
                val paymentData = PaymentData.getFromIntent(data!!)
                val token = paymentData?.toJson()?.let {
                    JSONObject(it).getJSONObject("paymentMethodData")
                        .getJSONObject("tokenizationData")
                        .getString("token")
                }
                GooglePayResult.Success(token ?: "")
            }
            Activity.RESULT_CANCELED -> GooglePayResult.Canceled
            else -> GooglePayResult.Failed(AutoResolveHelper.getStatusFromIntent(data))
        }
    }
}

Handling Payment Failures Safely

sealed class PaymentResult {
    object Success : PaymentResult()
    data class RequiresAction(val redirectUrl: String) : PaymentResult()  // 3D Secure
    data class Failed(val code: String, val message: String) : PaymentResult()
}

class CheckoutViewModel(
    private val paymentManager: PaymentManager,
    private val orderRepository: OrderRepository
) : ViewModel() {

    fun checkout(cartId: String) = viewModelScope.launch {
        _state.update { it.copy(isProcessing = true) }

        try {
            // 1. Create order (reserving inventory)
            val order = orderRepository.createOrder(cartId)

            // 2. Get payment intent from server
            val clientSecret = paymentManager.initiatePayment(order.totalCents)

            // 3. Present payment UI
            paymentManager.presentPaymentSheet(clientSecret)

            // Payment result handled asynchronously via callback
        } catch (e: Exception) {
            _state.update { it.copy(isProcessing = false, error = e.message) }
        }
    }

    // Called from payment result callback
    fun onPaymentSuccess(orderId: String) {
        viewModelScope.launch {
            // Confirm order server-side (server verifies payment with Stripe webhook)
            orderRepository.confirmOrder(orderId)
            _state.update { it.copy(isProcessing = false, isSuccess = true) }
        }
    }
}

Idempotency: Prevent Double Charges

// Always pass an idempotency key when creating payment intents server-side
// If the same request is retried (network failure after server received it),
// the server returns the same PaymentIntent instead of creating a new charge

data class CreatePaymentIntentRequest(
    val amount: Int,
    val currency: String,
    val orderId: String,         // use orderId as idempotency key
    val customerId: String
)

Key Takeaways

RuleWhy
Never handle raw card numbersPCI compliance — use PSP SDK tokenization
Client secret from serverServer creates PaymentIntent; client only presents it
Use Stripe PaymentSheetHandles 3DS, SCA, Google Pay, Apple Pay, all in one
Idempotency keyPrevents double charges on retry
Verify payment server-sideUse webhooks — don't trust client onPaymentSuccess alone
Order before paymentCreate the order first; confirm after payment succeeds

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Payments & PSP Integration | Android System Design | Android Engineers