-
Notifications
You must be signed in to change notification settings - Fork 4
Billing Integration
AdManageKit provides a comprehensive billing integration module (admanagekit-billing) that simplifies Google Play Billing Library v9 implementation.
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-billing:v4.4.5'
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-core:v4.4.5'val products = listOf(
// Consumable (coins, gems)
PurchaseItem("coins_100", TYPE_IAP.PURCHASE, PurchaseCategory.CONSUMABLE),
// Lifetime premium (disables ads)
PurchaseItem("lifetime", TYPE_IAP.PURCHASE, PurchaseCategory.LIFETIME_PREMIUM),
// Remove ads only
PurchaseItem("remove_ads", TYPE_IAP.PURCHASE, PurchaseCategory.REMOVE_ADS),
// Subscription with trial
PurchaseItem("premium_monthly", "free_trial", TYPE_IAP.SUBSCRIPTION)
)class MyApp : Application() {
override fun onCreate() {
super.onCreate()
// Inject your build state (a library AAR always has BuildConfig.DEBUG = false).
// In debug builds this registers a test product and routes purchase()/subscribe()
// to the dev purchase bottom sheet instead of the real Play flow. Call BEFORE initBilling.
AppPurchase.getInstance().setDebugMode(BuildConfig.DEBUG)
AppPurchase.getInstance().initBilling(this, products)
}
}Acknowledgment is automatic.
AppPurchaseacknowledges everyPURCHASEDpurchase before firing its callbacks, on both the new-purchase and restore paths — preventing Google Play's 3-day auto-refund of unacknowledged purchases. OnlyPurchaseState.PURCHASEDgrants entitlement (pending purchases do not). Consumables still require a manualconsumePurchase(productId)after granting.
Pending purchases need one thing from you (v4.4.3). A pending order (cash, bank transfer, parental approval) can complete hours or days later, while your app is closed.
onPurchasesUpdatedonly fires if the app happens to be running, so Play requires apps to re-query owned purchases on every foreground.AppPurchasequeries on billing init; if your process stays alive across the transition, that never runs again — so callrefreshPurchases()from your main activity'sonResume():override fun onResume() { super.onResume() AppPurchase.getInstance().refreshPurchases() }It acknowledges every unacknowledged
PURCHASEDpurchase it finds, which is what stops a completed pending order from being auto-cancelled on day 3.
// In-app purchase
AppPurchase.getInstance().purchase(activity, "remove_ads")
// Subscription
AppPurchase.getInstance().subscribe(activity, "premium_monthly")if (AppPurchase.getInstance().isPurchased()) {
// User has premium (subscription, lifetime, or remove_ads)
}val billing = AppPurchase.getInstance()
val name = billing.getProductName("premium_monthly") // "Monthly Premium"
val description = billing.getProductDescription("premium_monthly")
val hasTrial = billing.hasFreeTrial("premium_monthly") // true/false
val period = billing.getBillingPeriod("premium_monthly") // "P1M"For multi-offer subscriptions, use OfferInfo to read each offer's trial,
introductory, and base phases without parsing ProductDetails manually:
val trial = billing.getTrialOffer("premium_yearly")
trial?.let {
badge.text = "Free for ${it.trialPeriod}" // "P7D"
price.text = "${it.basePrice} / ${it.billingPeriod}"
}
val base = billing.getBaseOffer("premium_yearly") // non-promo offer
val all = billing.getOffers("premium_yearly") // every offersubscribe(activity, subsId) picks the offer for you (the configured trialId,
else Play's last offer), so on a multi-offer product it can charge for the
wrong plan. Pass the offer the user actually tapped:
val offers = billing.getOffers("premium_sub")
billing.subscribe(activity, offers[selectedIndex]) // exactly this planSee Subscription Offers for offer lookup, price normalization, savings badges, trial eligibility, and one-time product offers.
billing.setProductDetailsListener(object : ProductDetailsListener {
override fun onProductDetailsLoaded(
productType: String,
loaded: List<ProductDetails>,
unfetched: List<UnfetchedProduct>,
) {
unfetched.forEach { Log.e("Billing", "${it.productId}: status ${it.statusCode}") }
}
override fun onProductDetailsFailed(productType: String, responseCode: Int, debugMessage: String?) { }
})Register it before initBilling. Products land in unfetched when the id is
misspelled, the product is inactive in Play Console, or the signed-in account
cannot see the release track. isProductDetailsLoaded(id) and
areAllProductDetailsLoaded() distinguish "missing" from "not loaded yet".
// Google-recommended hashed identifiers — never raw account data. Max 64 chars.
billing.setObfuscatedAccountId(sha256(userId))
billing.setObfuscatedProfileId(sha256(profileId))
// Required EU disclosure when prices are personalized per user.
billing.setOfferPersonalized(true)Applied to every flow the library launches (purchase, subscribe,
updateSubscription). Set after sign-in; pass null on sign-out.
- Purchase Categories - Product classification system
- Subscription Offers - Offers, trials, intro pricing, price comparison
- Consumable Products - Handling consumables with manual consumption
- Subscriptions - Subscription lifecycle management
- Subscription Upgrades - Upgrade/downgrade handling
- Purchase History - Tracking purchases with PurchaseHistoryListener
- PurchaseResult API - Full API reference for PurchaseResult
- Server Verification - Server-side purchase verification
AdManageKit v3.3.4 | GitHub | API Docs | Report Issue | Buy me a coffee