
Android implementation of the OpenIAP specification using Google Play Billing.
Modern Android Kotlin library for in-app purchases using Google Play Billing Library v8.
Visit openiap.dev for complete documentation, guides, and the full OpenIAP specification.
OpenIAP GMS is a modern, type-safe Kotlin library that simplifies Google Play in-app billing integration. It provides a clean, coroutine-based API that handles all the complexity of Google Play Billing while offering robust error handling and real-time purchase tracking.
- π Google Play Billing v8 - Latest billing library with enhanced security
- β‘ Kotlin Coroutines - Modern async/await API
- π― Type Safe - Full Kotlin type safety with sealed classes
- π Real-time Events - Purchase update and error listeners
- π§΅ Thread Safe - Concurrent operations with proper synchronization
- π± Easy Integration - Simple singleton pattern with context management
- π‘οΈ Robust Error Handling - Comprehensive error types with detailed messages
- π Production Ready - Used in production apps
- Minimum SDK: 21 (Android 5.0)
- Compile SDK: 34+
- Google Play Billing: v8.0.0
- Kotlin: 1.9.20+
Add to your module's build.gradle.kts
:
dependencies {
implementation("io.github.hyochan.openiap:openiap-google:1.2.9")
}
Or build.gradle
:
dependencies {
implementation 'io.github.hyochan.openiap:openiap-google:1.2.9'
}
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
OpenIAP.initialize(this)
}
}
class MainActivity : AppCompatActivity() {
private lateinit var openIAP: OpenIAP
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
openIAP = OpenIAP.getInstance()
// Set up listeners
openIAP.addPurchaseUpdateListener { purchase ->
handlePurchaseUpdate(purchase)
}
openIAP.addPurchaseErrorListener { error ->
handlePurchaseError(error)
}
// Initialize connection
lifecycleScope.launch {
try {
val connected = openIAP.initConnection()
if (connected) {
loadProducts()
}
} catch (e: OpenIapError) {
// Handle connection error
}
}
}
private suspend fun loadProducts() {
try {
val products = openIAP.fetchProducts(listOf("premium_upgrade", "remove_ads"))
// Display products in UI
} catch (e: OpenIapError) {
// Handle error
}
}
private suspend fun purchaseProduct(productId: String) {
try {
openIAP.requestPurchase(
activity = this,
sku = productId
)
} catch (e: OpenIapError) {
// Handle purchase error
}
}
private fun handlePurchaseUpdate(purchase: OpenIapPurchase) {
when (purchase.purchaseState) {
PurchaseState.Purchased -> {
// Acknowledge or consume the purchase
lifecycleScope.launch {
try {
purchase.purchaseToken?.let { token ->
openIAP.acknowledgePurchase(token)
// Or for consumables: openIAP.consumePurchase(token)
}
} catch (e: OpenIapError) {
// Handle error
}
}
}
PurchaseState.Pending -> {
// Purchase is pending (e.g., awaiting payment)
}
// Handle other states...
}
}
override fun onDestroy() {
super.onDestroy()
openIAP.clearListeners()
openIAP.endConnection()
}
}
suspend fun initConnection(): Boolean
fun endConnection()
fun isReady(): Boolean
suspend fun fetchProducts(skus: List<String>): List<OpenIapProduct>
suspend fun fetchProducts(type: String, skus: List<String>): List<OpenIapProduct>
fun getCachedProduct(sku: String): ProductDetails?
fun getAllCachedProducts(): Map<String, ProductDetails>
suspend fun requestPurchase(
activity: Activity,
sku: String,
offerToken: String? = null,
obfuscatedAccountId: String? = null,
obfuscatedProfileId: String? = null
)
suspend fun requestPurchase(params: Map<String, Any?>, activity: Activity)
suspend fun finishTransaction(purchase: OpenIapPurchase, isConsumable: Boolean? = null)
suspend fun getAvailablePurchases(): List<OpenIapPurchase>
suspend fun getAvailablePurchases(options: Map<String, Any?>?): List<OpenIapPurchase> // options ignored on Android
suspend fun getAvailableItemsByType(type: String): List<OpenIapPurchase>
suspend fun acknowledgePurchase(purchaseToken: String): Boolean
suspend fun consumePurchase(purchaseToken: String): Boolean
Note: Use
"in-app"
for in-app product types. The legacy alias"inapp"
remains available for compatibility but will be removed in version 1.2.0.
suspend fun getStorefront(): String
suspend fun getActiveSubscriptions(subscriptionIds: List<String>? = null): List<OpenIapActiveSubscription>
suspend fun hasActiveSubscriptions(subscriptionIds: List<String>? = null): Boolean
fun deepLinkToSubscriptions(): Boolean
fun addPurchaseUpdateListener(listener: OpenIapPurchaseUpdateListener)
fun removePurchaseUpdateListener(listener: OpenIapPurchaseUpdateListener)
fun addPurchaseErrorListener(listener: OpenIapPurchaseErrorListener)
fun removePurchaseErrorListener(listener: OpenIapPurchaseErrorListener)
// Convenience methods
fun addListener(listener: OpenIapListener)
fun removeListener(listener: OpenIapListener)
fun clearListeners()
data class OpenIapProduct(
val id: String,
val title: String,
val description: String,
val price: Double?,
val displayPrice: String,
val currency: String,
val type: ProductType,
val platform: String = "android",
val displayName: String?,
val debugDescription: String?,
val nameAndroid: String?,
val oneTimePurchaseOfferDetails: OneTimePurchaseOfferDetails?,
val subscriptionOfferDetails: List<SubscriptionOfferDetails>?
)
data class OpenIapPurchase(
val id: String, // transactionId
val productId: String,
val ids: List<String>?, // alias of productIds
val transactionDate: Double,
val transactionReceipt: String,
val purchaseToken: String?,
val platform: String = "android",
val quantity: Int = 1,
val transactionId: String?,
val purchaseTime: Long,
val purchaseState: PurchaseState,
val isAutoRenewing: Boolean,
// ... Android-specific fields
val isAcknowledgedAndroid: Boolean?,
val autoRenewingAndroid: Boolean?,
// ... many more fields
)
sealed class OpenIapError : Exception {
object UserCancelled : OpenIapError()
object ItemAlreadyOwned : OpenIapError()
object ItemNotOwned : OpenIapError()
data class ProductNotFound(val productId: String) : OpenIapError()
data class PurchaseFailed(override val message: String) : OpenIapError()
// ... many more error types
}
- Initialize: Call
initConnection()
- Fetch Products: Use
fetchProducts()
to load available items - Request Purchase: Call
requestPurchase()
with the product SKU - Handle Events: Listen for purchase updates via listeners
- Process Purchase: Acknowledge non-consumables or consume consumables
- Server Verification: Always verify purchases on your backend
- Server-Side Verification: Always verify purchases on your backend server
- Acknowledge Promptly: Acknowledge non-consumable purchases within 3 days
- Consume Consumables: Consume consumable purchases after granting content
- Handle All States: Implement proper handling for all purchase states
- Error Handling: Implement comprehensive error handling
The library includes a comprehensive sample app demonstrating all features:
git clone https://github.com/hyodotdev/openiap-google.git
cd openiap-google
./gradlew :sample:installDebug
For development, use Google Play's test SKUs:
android.test.purchased
- Always succeedsandroid.test.canceled
- Always cancelsandroid.test.item_unavailable
- Always fails
For production testing, configure products in Google Play Console and use internal testing.
The included sample app demonstrates:
- β Connection management with retry logic
- β Product listing and purchase flow
- β Real-time purchase event handling
- β Purchase history and management
- β Error handling and user feedback
- β Android-specific billing features
try {
openIAP.requestPurchase(this, "premium_upgrade")
} catch (e: OpenIapError) {
when (e) {
OpenIapError.UserCancelled -> {
// User cancelled, no action needed
}
OpenIapError.ItemAlreadyOwned -> {
// Item already purchased
showMessage("You already own this item!")
}
is OpenIapError.ProductNotFound -> {
// Product not available
showError("Product ${e.productId} not found")
}
// Handle other error types...
else -> {
showError("Purchase failed: ${e.message}")
}
}
}
// Get subscription offers
val product = openIAP.getCachedProduct("monthly_subscription")
val offers = product?.subscriptionOfferDetails
// Purchase with specific offer
val offerToken = offers?.firstOrNull()?.offerToken
openIAP.requestPurchase(
activity = this,
sku = "monthly_subscription",
offerToken = offerToken
)
- This library requires Google Play Billing Library v8
- Test with real Google Play Console products for production
- Always verify purchases server-side for security
- Handle all purchase states properly
- Clean up listeners and connections in
onDestroy()
-
Product not found
- Ensure products are configured in Google Play Console
- App must be uploaded to Google Play Console (even as draft)
- Wait up to 24 hours for products to become available
-
Billing unavailable
- Verify Google Play Services are installed and updated
- Check that app is signed with release key for testing
- Ensure billing permissions are in AndroidManifest.xml
-
Purchase not triggering
- Use real device with Google Play Store
- Avoid emulators without Google Play Services
- Check that test account has payment method
Enable verbose logging to see detailed billing operations:
// In development builds
if (BuildConfig.DEBUG) {
Log.d("OpenIAP", "Debug mode enabled")
}
MIT License
Copyright (c) 2025 hyo.dev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Contributions are welcome! Please read our contributing guidelines and submit pull requests.
- Issues: GitHub Issues
- Discussions: OpenIAP Discussions