Kotlin Multiplatform Setup

kmp-iap brings OpenIAP-compliant in-app purchases to Kotlin Multiplatform projects. Android talks to Google Play Billing directly; iOS links the OpenIAP StoreKit framework, added with either CocoaPods or Swift Package Manager (see iOS Configuration below). Requires iOS 15.0+ and the Android minSdk shown in Android Configuration.

Prerequisites#

  • Kotlin 2.4.10, Gradle 9.3.0, and JDK 17+
  • Active Apple Developer account (for iOS)
  • Active Google Play Developer account (for Android)
  • Physical device for testing (simulators have limited IAP support)

Installation#

Add kmp-iap to your shared module's build.gradle.kts:

kt
val commonMain by getting {
    dependencies {
        implementation("io.github.hyochan:kmp-iap:3.5.0")
    }
}

Or if using version catalogs:

toml
# gradle/libs.versions.toml
[versions]
kmp-iap = "3.5.0"

[libraries]
kmp-iap = { module = "io.github.hyochan:kmp-iap", version.ref = "kmp-iap" }
kt
// build.gradle.kts
dependencies {
    implementation(libs.kmp.iap)
}

Platform Configuration#

iOS Configuration#

kmp-iap uses the OpenIAP framework on iOS. Choose CocoaPods or Swift Package Manager:

Option A: CocoaPods (Recommended)

Ensure your shared module has the CocoaPods plugin:

kt
// shared/build.gradle.kts
plugins {
    kotlin("multiplatform")
    kotlin("native.cocoapods")
}

kotlin {
    cocoapods {
        version = "1.0"
        ios.deploymentTarget = "15.0"
        framework {
            baseName = "ComposeApp" // or "shared"
            isStatic = true
        }
    }
}

Then run cd iosApp && pod install and always open .xcworkspace (not .xcodeproj).

Option B: Swift Package Manager

  1. In Xcode: File > Add Package Dependencies
  2. Enter URL: https://github.com/hyodotdev/openiap.git
  3. Select "Up to Next Major" version
  4. Add to your iOS app target
  5. Verify in Build Phases > Link Binary with Libraries

Enable In-App Purchase Capability

In Xcode: Target > Signing & Capabilities > + Capability > In-App Purchase

Configure Info.plist (optional)

Declaring itms-apps in iosApp/Info.plist is only needed when your own code checks App Store links before opening them — kmp-iap itself does not require it:

xml
<key>LSApplicationQueriesSchemes</key>
<array>
    <string>itms-apps</string>
</array>

Android Configuration#

Update your androidApp/build.gradle.kts:

kt
android {
    compileSdk = 36

    defaultConfig {
        minSdk = 24  // Required minimum
        targetSdk = 36
    }
}

ProGuard Rules (if using ProGuard)

# In-App Purchase
-keep class com.android.billingclient.** { *; }
-keep class io.github.hyochan.kmpiap.** { *; }
-keepattributes *Annotation*

Usage#

Results stream through Kotlin Flows: initialize the connection (initConnection), attach the purchase and error flows (purchaseUpdatedListener and purchaseErrorListener), then fetch and purchase (fetchProducts, requestPurchase, finishTransaction). A successful initConnection() followed by a non-empty fetchProducts() result is the quickest way to confirm the platform setup above is working. For the full flow, see the Purchase Guide.

Creating an Instance#

Two patterns are supported. The connection, fetch, and purchase APIs are suspend functions — call them from a coroutine scope:

kt
// Option 1: Global instance (convenient)
import io.github.hyochan.kmpiap.kmpIapInstance
scope.launch { kmpIapInstance.initConnection() }

// Option 2: Constructor (for DI / testing)
import io.github.hyochan.kmpiap.KmpIAP
val kmpIAP = KmpIAP()
scope.launch { kmpIAP.initConnection() }

See initConnection for parameters and per-store behavior.

Flow-Based Architecture#

KMP IAP delivers purchase results through hot Kotlin Flows — despite the names, purchaseUpdatedListener and purchaseErrorListener are Flows, not one-shot callbacks. Collect both in a long-lived coroutine scope before requesting a purchase; events are emitted as they occur.

kt
import io.github.hyochan.kmpiap.KmpIAP
import kotlinx.coroutines.*

val kmpIAP = KmpIAP()
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())

scope.launch { kmpIAP.initConnection() }

// Collect in separate coroutines (collect is suspending and never returns)
scope.launch {
    kmpIAP.purchaseUpdatedListener.collect { purchase ->
        // Validate receipt with your backend or IAPKit
        // CRITICAL: Android auto-refunds after 3 days if not called!
        kmpIAP.finishTransaction(purchase = purchase, isConsumable = true)
    }
}

scope.launch {
    kmpIAP.purchaseErrorListener.collect { error ->
        println("Error: ${error.code} - ${error.message}")
    }
}

For the possible error.code values, see Error Codes.

Products and Purchase#

Fetch products once the connection is up, then request a purchase — the result arrives on purchaseUpdatedListener, not as a return value. See fetchProducts and requestPurchase for parameters and per-store behavior.

kt
import io.github.hyochan.kmpiap.openiap.*

scope.launch {
    // Fetch products
    val products = kmpIAP.fetchProducts(
        ProductRequest(
            skus = listOf("premium", "coins_100"),
            type = ProductQueryType.InApp
        )
    )

    // Purchase — the result arrives on purchaseUpdatedListener
    kmpIAP.requestPurchase(
        RequestPurchaseProps(
            request = RequestPurchaseProps.Request.Purchase(
                RequestPurchasePropsByPlatforms(
                    apple = RequestPurchaseIosProps(
                        sku = "premium"
                    ),
                    google = RequestPurchaseAndroidProps(
                        skus = listOf("premium")
                    )
                )
            ),
            type = ProductQueryType.InApp
        )
    )
}

Call endConnection() when the owning screen or scope is disposed — not right after requestPurchase, or the connection may close before the purchase result arrives.

Troubleshooting#

Linking error: Undefined symbol OpenIapModule (iOS)

This means the OpenIAP framework isn't linked.

  • CocoaPods: Run cd iosApp && pod install and open .xcworkspace (not .xcodeproj)
  • SPM: Verify OpenIAP appears in Build Phases > Link Binary with Libraries
  • Clean build folder and rebuild

Products not found

  • Ensure all agreements are signed in App Store Connect / Google Play Console
  • Verify banking, legal, and tax information is complete and approved
  • Check that bundle ID / package name matches exactly
  • Products must be in "Ready to Submit" status (Apple) or "Active" (Google)
  • Wait 15-30 minutes after creating products before testing

Billing unavailable (Android)

  • Test on a real device, not an emulator
  • Ensure Google Play Store is installed and updated
  • App must be signed with the same certificate uploaded to Play Console

Next Steps#