How I Tackled Payment Processing in Kotlin Multiplatform Using Android Native Code

Experience Exchange

Hey devs!

I recently shared my experience building an app with Kotlin Multiplatform (KMP), focusing on the parts that still require native code. You can check out the original post here: link.

A big part of the discussion was around Billing, which, despite KMP’s capabilities, still requires native implementations. Here’s how I tackled payment processing in a KMP project with native support.

🛠️ Architecture Breakdown

I set up a BillingDelegate interface in the shared KMP module:

interface BillingDelegate {
    fun requestPricingUpdate()
    fun subscribe(period: Subscription.Period)
    fun isBillingSupported(): Boolean
    fun openPromoRedeem()

    interface StateRepository {
        fun update(pricingResult: List<Subscription>)
        fun getStream(): Flow<BillingState>
        fun onPurchaseEvent(state: PurchaseState)
        fun onError()
    }
}

Sealed Classes for State Management:

sealed class BillingState {
    object Loading : BillingState()
    object Error : BillingState()
    object PurchaseCompleted : BillingState()
    object PurchaseCanceled : BillingState()
    data class Data(
        val subscriptions: List<Subscription>,
        val offers: Map<Subscription.Period, Offer>
    ) : BillingState()
}

sealed interface PurchaseState {
    object Canceled : PurchaseState
    data class Completed(val transactionId: String? = null) : PurchaseState
}

Android-Specific Implementation:

@Single
class GooglePlayBillingDelegate(
    private val activityProvider: ActivityProvider,
    private val context: Context,
    private val billingStateRepository: BillingDelegate.StateRepository,
    private val pricingMapper: PricingMapper,
    private val periodMapper: PeriodMapper,
    private val userRepository: UserRepository,
    private val analytics: AnalyticsManager,
) : BillingDelegate {
    private val purchasesUpdatedListener =
        PurchasesUpdatedListener { billingResult, purchases ->
            analytics.log("$billingResult", LogLevel.DEBUG)
            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                billingStateRepository.onPurchaseEvent(PurchaseState.Completed())
            } else {
                billingStateRepository.onPurchaseEvent(PurchaseState.Canceled)
            }
        }
    ...
}

When I need to fetch product details, I request them through the BillingDelegate, which forwards the call to the native Android implementation:

override fun requestPricingUpdate() {
    bgScope.launch {
        ensureConnection()
        val result = billingClient.getProductDetails(...)
        with(result.productDetailsList?.firstOrNull()) {
            productDetails = this
            val subscriptions = pricingMapper.map(this)
            billingStateRepository.update(subscriptions)
        }
    }
}

You could notice I also have a method for promo code redemption in interface. This is currently just a placeholder on Android, but it’s necessary on iOS because Apple has a dedicated flow for redeeming promo codes.

Shared State Repository (KMP):

The magic happens in the shared module with the StateRepository, which is 100% common code:

@Single
class BillingDelegateInMemoryStateRepository(
    private val userRepository: UserRepository,
) : BillingDelegate.StateRepository {
    private val billingState = MutableStateFlow<BillingState>(BillingState.Loading)
    ...
}

Flow of Operations Recap:

  1. Kotlin + Compose Multiplatform requests product details via BillingDelegate.