Skip to content

Billing Integration

github-actions[bot] edited this page Aug 27, 2026 · 8 revisions

Billing Integration

AdManageKit provides a comprehensive billing integration module (admanagekit-billing) that simplifies Google Play Billing Library v9 implementation.

Quick Start

1. Add Dependency

implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-billing:v4.4.5'
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-core:v4.4.5'

2. Define Products

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

3. Initialize

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. AppPurchase acknowledges every PURCHASED purchase before firing its callbacks, on both the new-purchase and restore paths — preventing Google Play's 3-day auto-refund of unacknowledged purchases. Only PurchaseState.PURCHASED grants entitlement (pending purchases do not). Consumables still require a manual consumePurchase(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. onPurchasesUpdated only fires if the app happens to be running, so Play requires apps to re-query owned purchases on every foreground. AppPurchase queries on billing init; if your process stays alive across the transition, that never runs again — so call refreshPurchases() from your main activity's onResume():

override fun onResume() {
    super.onResume()
    AppPurchase.getInstance().refreshPurchases()
}

It acknowledges every unacknowledged PURCHASED purchase it finds, which is what stops a completed pending order from being auto-cancelled on day 3.

4. Make Purchases

// In-app purchase
AppPurchase.getInstance().purchase(activity, "remove_ads")

// Subscription
AppPurchase.getInstance().subscribe(activity, "premium_monthly")

5. Check Purchase Status

if (AppPurchase.getInstance().isPurchased()) {
    // User has premium (subscription, lifetime, or remove_ads)
}

6. Product Metadata (v3.4.1+)

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"

7. Structured Offers (v3.5.7+)

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 offer

8. Buy a Specific Offer (v4.4.0+)

subscribe(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 plan

See Subscription Offers for offer lookup, price normalization, savings badges, trial eligibility, and one-time product offers.

9. Diagnose an Empty Paywall (v4.4.0+)

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".

10. Fraud Prevention & EU Disclosure (v4.4.0+)

// 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.

Pages

Clone this wiki locally