Use this page with AI

Copy this into your coding assistant and add your request.

Read https://openiap.dev/docs/setup/kmp and https://openiap.dev/llms.txt. Follow the reading instructions, detailed reference, and linked guides relevant to my task before making changes.
Inspect my existing project and reuse its framework and conventions. Ask me for missing product decisions. Implement the requested behavior and run the applicable checks.
Show the working result, the commands and actual test results, and any remaining limitations. Keep your explanation brief.

My request: [describe what customers should be able to do]
See an example request →

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.

For an independent store on Apple or Android, follow the community provider guide. Link its native package and select its factory in your app; purchase APIs keep the platform's existing shape and preserve the provider's store id.

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:4.0.0")
    }
}

Or if using version catalogs:

toml
# gradle/libs.versions.toml
[versions]
kmp-iap = "4.0.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
    }
}

Pick the Android store

kmp-iap publishes Play, Horizon, Amazon, and provider builds of its Android library, and Gradle stops with Cannot choose between the following variants of io.github.hyochan:kmp-iap until the build names one. Apply the OpenIAP Gradle plugin once in settings.gradle.kts; it reaches every Android module of that build, including an app module that sees kmp-iap only through a shared module. A build pulled in with includeBuild applies it in its own settings. It links Play by default, the connected Quest or Fire device's store on a debug build, and the store openiapStore pins, which is how release builds choose.

kt
// settings.gradle.kts — keep mavenCentral() in pluginManagement.repositories
plugins {
    id("io.github.hyochan.openiap") version "4.0.0"
}
props
# gradle.properties — only for a build that must not follow the device
openiapStore=horizon

Without the plugin, name the store in every Android module: missingDimensionStrategy("platform", "play") in the defaultConfig of application and library modules, or localDependencySelection { productFlavorDimension("platform") { selectFrom.set(listOf("play")) } } inside the Kotlin Multiplatform Android library block (AGP 8.12 or later; AGP 9 removed the older dependencyVariantSelection). A module that declares its own platform flavors keeps choosing per flavor. See How the Store Is Selected for the full rule. At runtime, getStore() reports the linked store: Store.HORIZON on a Horizon build, Store.AMAZON on an Amazon build, Store.PLAY_STORE on a Play build, Store.APP_STORE on iOS, and Store.UNKNOWN on a community-provider build or when the Apple provider selection fails. From 4.0.0, handle UNKNOWN in every exhaustive when; see the upgrade notes.

R8 and ProGuard

No rules to add. From 3.6.1, kmp-iap and the openiap store artifact ship the R8 rules each store needs, so minified release builds work as they are.

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() }

Keep one live client per process. On Meta Horizon a second live client kills the process when it queries purchases.

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. Handle purchases on purchaseUpdatedListener and cancellations on purchaseErrorListener. Use the nullable requestPurchase(props) form below for these event-driven screens; the DSL form requires a purchase result and throws when none is returned. 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#