Skip to main content
Version: v1.0.0-rc (Current)

Basic Setup

This guide walks you through the complete setup process for KMP IAP in your Kotlin Multiplatform project.

Project Configuration

1. Add Dependencies

In your shared module's build.gradle.kts:

kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.github.hyochan:kmp-iap:1.0.0-rc.2")
}
}
}
}

2. Platform Configuration

Android Configuration

In your Android app module's build.gradle.kts:

android {
defaultConfig {
// Ensure minimum SDK is 24 or higher
minSdk = 24
}
}

Add to your AndroidManifest.xml:

<uses-permission android:name="com.android.vending.BILLING" />

iOS Configuration

  1. In Xcode, add the In-App Purchase capability:

    • Select your app target
    • Go to "Signing & Capabilities"
    • Click "+" and add "In-App Purchase"
  2. Configure your products in App Store Connect

Implementation

1. Create IAP Manager

import io.github.hyochan.kmpiap.KmpIAP
import io.github.hyochan.kmpiap.types.*

```kotlin
import io.github.hyochan.kmpiap.KmpIAP
import io.github.hyochan.kmpiap.types.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

object IAPManager {
private val kmpIAP = KmpIAP() // Create instance
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())

// State management
private val _isConnected = MutableStateFlow(false)
val isConnected: StateFlow<Boolean> = _isConnected.asStateFlow()

private val _products = MutableStateFlow<List<Product>>(emptyList())
val products: StateFlow<List<Product>> = _products.asStateFlow()

private val _subscriptions = MutableStateFlow<List<Product>>(emptyList())
val subscriptions: StateFlow<List<Product>> = _subscriptions.asStateFlow()

fun initialize() {
scope.launch {
try {
kmpIAP.initConnection()
_isConnected.value = true

// Set up purchase listeners
setupPurchaseListeners()

// Load products
loadProducts()
} catch (e: Exception) {
println("IAP initialization failed: ${e.message}")
_isConnected.value = false
}
}
}

private fun setupPurchaseListeners() {
scope.launch {
// Listen for purchase updates
kmpIAP.purchaseUpdatedListener.collect { purchase ->
handlePurchaseUpdate(purchase)
}
}

scope.launch {
// Listen for purchase errors
kmpIAP.purchaseErrorListener.collect { error ->
handlePurchaseError(error)
}
}
}

private suspend fun loadProducts() {
try {
// Load in-app products
val productList = kmpIAP.requestProducts(
ProductRequest(
skus = listOf("remove_ads", "premium_features"),
type = ProductType.INAPP
)
)
_products.value = productList

// Load subscriptions
val subsList = kmpIAP.requestProducts(
ProductRequest(
skus = listOf("monthly_sub", "yearly_sub"),
type = ProductType.SUBS
)
)
_subscriptions.value = subsList
} catch (e: Exception) {
println("Failed to load products: ${e.message}")
}
}

suspend fun purchaseProduct(productId: String) {
kmpIAP.requestPurchase(
UnifiedPurchaseRequest(
sku = productId,
quantity = 1
)
)
}

suspend fun purchaseSubscription(productId: String) {
kmpIAP.requestPurchase(
UnifiedPurchaseRequest(
sku = productId,
quantity = 1
)
)
}

private suspend fun handlePurchaseUpdate(purchase: Purchase) {
// Purchase is valid if returned from purchaseUpdatedListener flow
// Process the purchase
// Verify purchase with your backend
val isValid = verifyPurchaseWithBackend(purchase)

if (isValid) {
// Grant entitlement
grantEntitlement(purchase.productId)

// Finish transaction
kmpIAP.finishTransaction(
purchase,
isConsumable = isConsumableProduct(purchase.productId)
)
}
}
}

private fun handlePurchaseError(error: PurchaseError) {
when (error.code) {
ErrorCode.USER_CANCELLED -> {
// User cancelled, no action needed
}
ErrorCode.PRODUCT_ALREADY_OWNED -> {
// Item already owned, restore it
restorePurchases()
}
else -> {
// Show error to user
showError(error.message)
}
}
}

suspend fun restorePurchases() {
val purchases = kmpIAP.getAvailablePurchases()
purchases.forEach { purchase ->
grantEntitlement(purchase.productId)
}
}

private suspend fun verifyPurchaseWithBackend(purchase: Purchase): Boolean {
// Implement your backend verification
// This is a simplified example
return true
}

private fun grantEntitlement(productId: String) {
// Grant the appropriate entitlement based on productId
when (productId) {
"remove_ads" -> UserSettings.adsRemoved = true
"premium_features" -> UserSettings.isPremium = true
// Handle other products
}
}

private fun isConsumableProduct(productId: String): Boolean {
// Define which products are consumable
return when (productId) {
"coins_pack_100", "coins_pack_500" -> true
else -> false
}
}

private fun notifyUserOfPendingPurchase() {
// Notify user that purchase is pending
}

private fun showError(message: String) {
// Show error message to user
}

fun cleanup() {
scope.cancel()
runBlocking {
kmpIAP.endConnection()
}
}
}

// Simple user settings example
object UserSettings {
var adsRemoved: Boolean = false
var isPremium: Boolean = false
}

2. Initialize in Your App

Android

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Initialize IAP
IAPManager.initialize()

setContent {
MyApp()
}
}

override fun onDestroy() {
super.onDestroy()
IAPManager.cleanup()
}
}

iOS

import UIKit
import shared

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Initialize IAP
IAPManager.shared.initialize()
return true
}
}

3. Use in Your UI

@Composable
fun StoreScreen() {
val products by IAPManager.products.collectAsState()
val subscriptions by IAPManager.subscriptions.collectAsState()

LazyColumn {
items(products) { product ->
ProductCard(
product = product,
onPurchase = {
lifecycleScope.launch {
IAPManager.purchaseProduct(product.productId)
}
}
)
}

items(subscriptions) { subscription ->
SubscriptionCard(
subscription = subscription,
onPurchase = {
lifecycleScope.launch {
IAPManager.purchaseSubscription(subscription.productId)
}
}
)
}
}
}

@Composable
fun ProductCard(product: Product, onPurchase: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = product.title,
style = MaterialTheme.typography.h6
)
Text(
text = product.description,
style = MaterialTheme.typography.body2
)
}
Button(onClick = onPurchase) {
Text(product.localizedPrice)
}
}
}
}

Testing

Android Testing

  1. Upload your app to Google Play Console (at least Internal Testing)
  2. Add test accounts in Google Play Console
  3. Test with a signed APK

iOS Testing

  1. Create sandbox test accounts in App Store Connect
  2. Use StoreKit Configuration file for local testing
  3. Test on a real device for best results

Common Issues

Connection Issues

  • Ensure you're calling initConnection() before any other operations
  • Check network connectivity
  • Verify store configuration

Product Not Found

  • Verify product IDs match exactly with store configuration
  • Ensure products are active in store console
  • Wait for products to propagate (can take up to 24 hours)

Purchase Failures

  • Check if user is signed in to store account
  • Verify payment methods are set up
  • Ensure app is properly signed for release

Next Steps