Skip to content

Subscriptions

Muhammad Hammad edited this page Jul 28, 2026 · 4 revisions

Subscriptions

AdManageKit provides comprehensive subscription management including state tracking, upgrade/downgrade support, and lifecycle management.

Setting Up Subscriptions

Define Subscription Products

val products = listOf(
    // Subscription with free trial offer
    PurchaseItem("premium_monthly", "free_trial_7d", TYPE_IAP.SUBSCRIPTION),

    // Subscription without trial
    PurchaseItem("premium_yearly", null, TYPE_IAP.SUBSCRIPTION),

    // Multiple tiers
    PurchaseItem("basic_monthly", null, TYPE_IAP.SUBSCRIPTION),
    PurchaseItem("pro_monthly", "intro_offer", TYPE_IAP.SUBSCRIPTION)
)

Initialize

AppPurchase.getInstance().initBilling(application, products)

Subscribing

// Start subscription flow
AppPurchase.getInstance().subscribe(activity, "premium_monthly")

Subscription States

State Enum

enum class SubscriptionState {
    ACTIVE,          // Subscribed and will renew
    CANCELLED,       // Cancelled but still has access
    GRACE_PERIOD,    // Payment issue, still has access (server-side only)
    ON_HOLD,         // Payment declined, no access (client-side since v4.4.0)
    PAUSED,          // User paused (server-side only)
    EXPIRED,         // Subscription ended
    NOT_SUBSCRIPTION // Product is not a subscription
}

Client-Side Detection

State Detectable Client-Side How
ACTIVE Yes isAutoRenewing = true
CANCELLED Yes isAutoRenewing = false, still in purchase list
GRACE_PERIOD No Requires server API
ON_HOLD Yes (v4.4.0+) isSuspended = true (Play Billing 9)
PAUSED No Requires server API
EXPIRED Yes Not returned by queryPurchasesAsync

Account Hold (v4.4.0+)

When Google Play cannot charge a subscriber's payment method, the subscription enters account hold. The purchase record still exists, but the user has not paid and must not keep premium access.

Since v4.4.0 this is detected client-side; earlier versions needed a server round-trip through the Play Developer API.

val billing = AppPurchase.getInstance()

billing.hasSubscriptionOnHold()      // any subscription on hold
purchaseResult.isSuspended           // this specific subscription
purchaseResult.subscriptionState     // SubscriptionState.ON_HOLD

Behavior change in 4.4.0. isSubscriptionActive() now returns false for an on-hold subscription, and getSubscriptionState() returns ON_HOLD rather than ACTIVE/CANCELLED. This matches Google's requirement — an on-hold user's payment was declined. If your app deliberately keeps serving these users, check isSuspended() explicitly rather than relying on the old behavior.

Recovering declined payments

Play can show the user an in-app flow to fix their payment method. Google recommends calling this on foreground entry for apps that sell subscriptions:

billing.showInAppMessages(activity, object : InAppMessageListener {
    override fun onSubscriptionRecovered(purchaseToken: String) {
        // AppPurchase has already refreshed — isPurchased() is correct here.
        refreshPremiumUi()
    }
    override fun onNoActionNeeded() { }
})

Pending plan changes

A subscription upgrade or downgrade the user started but has not paid for yet:

billing.hasPendingSubscriptionChange()
purchaseResult.hasPendingPurchaseUpdate()
purchaseResult.pendingProductIds
purchaseResult.pendingPurchaseToken

The current plan remains the entitlement in force until the change completes, so show "plan change pending" rather than switching the UI to the new tier.

Checking Subscription Status

Basic Checks

// Any subscription active?
if (AppPurchase.getInstance().isSubscribed()) {
    showPremiumContent()
}

// Specific subscription active?
if (AppPurchase.getInstance().isSubscribed("premium_yearly")) {
    showYearlyBenefits()
}

Get Subscription State

val state = AppPurchase.getInstance().getSubscriptionState("premium_monthly")

when (state) {
    SubscriptionState.ACTIVE -> {
        // User is subscribed and will renew
        showPremiumUI()
    }
    SubscriptionState.CANCELLED -> {
        // User cancelled but still has access until expiration
        showRenewalPrompt()
    }
    SubscriptionState.EXPIRED -> {
        // Subscription ended
        showSubscribeButton()
    }
    else -> {
        // Not subscribed or other state
    }
}

Additional Checks

// Check if any subscription is cancelled (but still active)
if (AppPurchase.getInstance().hasSubscriptionCancelled()) {
    showResubscribePrompt()
}

// Check if all subscriptions will renew
if (AppPurchase.getInstance().willSubscriptionsRenew()) {
    // User is committed subscriber
}

// Get all active subscriptions
val subscriptions = AppPurchase.getInstance().getActiveSubscriptions()
for (sub in subscriptions) {
    println("${sub.getFirstProductId()}: ${sub.getSubscriptionStateString()}")
}

Subscription Details

Get Subscription PurchaseResult

val subscription = AppPurchase.getInstance().getSubscription("premium_monthly")
if (subscription != null) {
    // Basic info
    val orderId = subscription.orderId
    val purchaseTime = subscription.getPurchaseTimeFormatted()

    // State info
    val isActive = subscription.isSubscriptionActive()
    val willRenew = subscription.willSubscriptionRenew()
    val isCancelled = subscription.isSubscriptionCancelled()
    val stateString = subscription.getSubscriptionStateString()

    // Display
    showSubscriptionInfo(
        status = stateString,      // "Active", "Cancelled (access until expiration)"
        since = purchaseTime,      // "2024-01-15 14:30:00"
        willRenew = willRenew      // true/false
    )
}

Subscription Pricing

// Get formatted price
val monthlyPrice = AppPurchase.getInstance().getPriceSub("premium_monthly")
// "$9.99"

val yearlyPrice = AppPurchase.getInstance().getPriceSub("premium_yearly")
// "$99.99"

Trial Offers

Specify trial offer ID when creating PurchaseItem:

// The second parameter is the offer ID from Google Play Console
PurchaseItem("premium_monthly", "free_trial_7d", TYPE_IAP.SUBSCRIPTION)

The library automatically selects the matching offer when subscribing.

Reading Trial / Intro / Base Pricing (v3.5.7+)

Use OfferInfo to inspect each offer's pricing phases for the paywall UI:

val billing = AppPurchase.getInstance()

billing.getTrialOffer("premium_monthly")?.let { trial ->
    badge.text = "Free for ${trial.trialPeriod}"          // "P7D"
    afterTrial.text = "${trial.basePrice}/${trial.billingPeriod}"
}

billing.getBaseOffer("premium_monthly")?.let { base ->
    price.text = base.basePrice                           // "$9.99"
}

// Iterate every offer (trial, intro, base, etc.)
for (offer in billing.getOffers("premium_monthly")) {
    // offer.isFreeTrial, offer.hasIntroPrice, offer.introPrice, offer.offerTags ...
}

Since v4.4.0 you can also buy a specific offer rather than letting the library pick one, and compare plans across billing cadences:

billing.subscribe(activity, offer)                              // buy exactly this offer
billing.isEligibleForFreeTrial("premium_monthly")               // Play filters per account
billing.getSavingsPercent("premium_monthly", "premium_yearly")  // 50 → "Save 50%"
billing.getFormattedPricePerMonth("premium_yearly")             // "$5.00"

See Subscription Offers for the full offer API, and API Reference for the OfferInfo schema.

Handling Subscription Changes

See Subscription Upgrades for upgrade/downgrade handling.

Best Practices

1. Always Show Subscription State

fun updateSubscriptionUI() {
    val state = AppPurchase.getInstance().getSubscriptionState("premium_monthly")

    subscriptionBadge.text = when (state) {
        SubscriptionState.ACTIVE -> "Premium Member"
        SubscriptionState.CANCELLED -> "Premium (Expires Soon)"
        else -> "Free User"
    }
}

2. Prompt Cancelled Users to Resubscribe

if (AppPurchase.getInstance().hasSubscriptionCancelled()) {
    showDialog(
        title = "Your subscription is ending",
        message = "Renew now to keep premium features",
        action = "Renew" to { reopenSubscription() }
    )
}

fun reopenSubscription() {
    // This will show the resubscribe flow in Google Play
    AppPurchase.getInstance().subscribe(activity, "premium_monthly")
}

3. Restore Purchases on App Start

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // Verify purchases to restore subscription state
    AppPurchase.getInstance().verifyPurchased(true)
}

4. Handle Grace Period (Server-Side)

For grace period detection, implement server-side verification using Google Play Developer API and Real-time Developer Notifications (RTDN).

Subscription Expiry Verification

The Google Play Billing Library does NOT provide subscription expiry dates client-side. To get accurate expiry dates, you need server-side verification.

Setting Up Verification Callback

// In Application.onCreate() or early initialization
AppPurchase.getInstance().setSubscriptionVerificationCallback { packageName, subscriptionId, purchaseToken, listener ->
    // Call your backend API
    yourBackendApi.verifySubscription(packageName, subscriptionId, purchaseToken,
        onSuccess = { expiryTimeMillis, isAutoRenewing ->
            val details = SubscriptionVerificationCallback.SubscriptionDetails.Builder()
                .setExpiryTimeMillis(expiryTimeMillis)
                .setAutoRenewing(isAutoRenewing)
                .build()
            listener.onVerified(details)
        },
        onError = { error ->
            listener.onVerificationFailed(error)
        }
    )
}

Verifying a Subscription

AppPurchase.getInstance().verifySubscription("premium_monthly",
    object : AppPurchase.SubscriptionVerificationListener {
        override fun onVerified(subscription: PurchaseResult) {
            // Expiry info now available
            val expiryDate = subscription.getExpiryTimeFormatted("dd MMM yyyy")
            val remainingDays = subscription.getRemainingDays()
            val isExpired = subscription.isExpired()

            updateUI(expiryDate, remainingDays)
        }

        override fun onVerificationFailed(errorMessage: String?) {
            Log.e("Subscription", "Verification failed: $errorMessage")
        }
    }
)

// Verify all active subscriptions at once
AppPurchase.getInstance().verifyAllSubscriptions(listener)

Getting Expiry Information

After verification, expiry data is stored and accessible:

// Get expiry time in milliseconds
val expiryMillis = AppPurchase.getInstance().getSubscriptionExpiryTime("premium_monthly")

// Get formatted expiry date
val expiryDate = AppPurchase.getInstance().getSubscriptionExpiryTimeFormatted("premium_monthly")
// "2024-03-15 14:30:00"

// Custom format
val customFormat = AppPurchase.getInstance().getSubscriptionExpiryTimeFormatted("premium_monthly", "dd MMM yyyy")
// "15 Mar 2024"

// Get remaining days
val daysLeft = AppPurchase.getInstance().getSubscriptionRemainingDays("premium_monthly")
// 30

// Check if expired
val isExpired = AppPurchase.getInstance().isSubscriptionExpired("premium_monthly")
// false

Using PurchaseResult Directly

val subscription = AppPurchase.getInstance().getSubscription("premium_monthly")
if (subscription != null && subscription.isExpiryVerified()) {
    // Expiry data available
    val expiryDate = subscription.getExpiryDate()           // Date object
    val expiryFormatted = subscription.getExpiryTimeFormatted()  // String
    val remainingDays = subscription.getRemainingDays()     // Int (-1 if not verified)
    val remainingMillis = subscription.getRemainingTime()   // Long (-1 if not verified)
    val isExpired = subscription.isExpired()               // Boolean
}

Backend API Implementation

Your backend should call Google Play Developer API:

GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptionsv2/tokens/{purchaseToken}

Response includes:

  • expiryTime - RFC 3339 timestamp
  • subscriptionState - ACTIVE, CANCELED, IN_GRACE_PERIOD, ON_HOLD, PAUSED, EXPIRED
  • linkedPurchaseToken - For upgrade/downgrade tracking

Example: Show Subscription Status UI

fun updateSubscriptionUI() {
    val subscription = AppPurchase.getInstance().getSubscription("premium_monthly")

    if (subscription == null) {
        showNotSubscribedUI()
        return
    }

    if (!subscription.isExpiryVerified()) {
        // Verify first
        AppPurchase.getInstance().verifySubscription("premium_monthly",
            object : AppPurchase.SubscriptionVerificationListener {
                override fun onVerified(sub: PurchaseResult) {
                    showSubscriptionDetails(sub)
                }
                override fun onVerificationFailed(error: String?) {
                    showBasicSubscriptionUI(subscription)
                }
            }
        )
    } else {
        showSubscriptionDetails(subscription)
    }
}

fun showSubscriptionDetails(subscription: PurchaseResult) {
    val daysLeft = subscription.getRemainingDays()

    when {
        subscription.isExpired() -> {
            statusText.text = "Subscription Expired"
            renewButton.visibility = View.VISIBLE
        }
        daysLeft <= 7 && !subscription.isAutoRenewing -> {
            statusText.text = "Expires in $daysLeft days"
            renewPrompt.visibility = View.VISIBLE
        }
        else -> {
            statusText.text = "Premium until ${subscription.getExpiryTimeFormatted("dd MMM yyyy")}"
        }
    }
}

Clone this wiki locally