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
| Rule | Why |
|---|---|
| Never handle raw card numbers | PCI compliance — use PSP SDK tokenization |
| Client secret from server | Server creates PaymentIntent; client only presents it |
| Use Stripe PaymentSheet | Handles 3DS, SCA, Google Pay, Apple Pay, all in one |
| Idempotency key | Prevents double charges on retry |
| Verify payment server-side | Use webhooks — don't trust client onPaymentSuccess alone |
| Order before payment | Create the order first; confirm after payment succeeds |