Purchases
Complete guide to implementing in-app purchases with kmp-iap v1.0.0-beta.2, covering everything from basic setup to advanced purchase handling using Kotlin Multiplatform.
Purchase Flow Overview
The in-app purchase flow follows this standardized pattern:
- Initialize Connection - Establish connection with the store
- Setup State Observers - Monitor purchase states via StateFlow
- Load Products - Fetch product information from the store
- Request Purchase - Initiate purchase flow
- Handle Updates - Process purchase results via StateFlow
- Deliver Content - Provide purchased content to user
- Finish Transaction - Complete the transaction with the store
Key Concepts
Purchase Types
- Consumable: Can be purchased multiple times (coins, gems, lives)
- Non-Consumable: Purchased once, owned forever (premium features, ad removal)
- Subscriptions: Recurring purchases with auto-renewal
Platform Differences
- iOS: Uses StoreKit 2 (iOS 15.0+)
- Android: Uses Google Play Billing Client v8
- Both platforms use the same API surface in kmp-iap
Basic Purchase Flow
1. Setup Purchase Observers
Before making any purchases, set up StateFlow observers to handle purchase updates and errors:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import io.github.hyochan.kmpiap.KmpIAP
import io.github.hyochan.kmpiap.data.*
class PurchaseHandler(
private val scope: CoroutineScope
) {
fun setupPurchaseObservers() {
// Observe successful purchases
scope.launch {
kmpIAP.currentPurchase.collectLatest { purchase ->
purchase?.let {
println("Purchase update received: ${it.productId}")
handlePurchaseUpdate(it)
}
}
}
// Observe purchase errors
scope.launch {
kmpIAP.currentError.collectLatest { error ->
error?.let {
println("Purchase failed: ${it.message}")
handlePurchaseError(it)
kmpIAP.clearError()
}
}
}
}
fun dispose() {
kmpIAP.dispose()
}
}
2. Using with ViewModel (Recommended)
For a more structured approach, use this purchase handler pattern:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
class ProductsViewModel : ViewModel() {
private val productIds = listOf(
"dev.hyo.martie.10bulbs",
"dev.hyo.martie.30bulbs"
)
data class PurchaseState(
val isProcessing: Boolean = false,
val purchaseResult: String? = null,
val products: List<Product> = emptyList()
)
private val kmpIAP = KmpIAP()
private val _state = MutableStateFlow(PurchaseState())
val state: StateFlow<PurchaseState> = _state.asStateFlow()
init {
setupPurchaseObservers()
// Initialize connection and load products
viewModelScope.launch {
val connected = kmpIAP.initConnection()
if (connected) {
loadProducts()
}
}
}
override fun onCleared() {
super.onCleared()
kmpIAP.dispose()
}
// Purchase observer setup...
}
3. Request a Purchase
Use the unified API for initiating purchases:
suspend fun handlePurchase(productId: String) {
try {
_state.update {
it.copy(
isProcessing = true,
purchaseResult = "Processing purchase..."
)
}
// Request purchase
kmpIAP.requestPurchase(
sku = productId,
quantityIOS = 1, // iOS only
obfuscatedAccountIdAndroid = getUserId() // Android only
)
// Result will be emitted via currentPurchase StateFlow
} catch (error: PurchaseError) {
_state.update {
it.copy(
isProcessing = false,
purchaseResult = "❌ Purchase failed: ${error.message}"
)
}
}
}