RORK LABJP
RORKMAX — Rork Max generates pure Swift instead of React Native, enabling true native apps across iPhone, iPad, Watch, TV, Vision Pro, and iMessageAPPLE — Rork's 2026 direction has a clear theme of native empowerment across the Apple ecosystemEXPO — Standard builds run on React Native and Expo, so you're left with a real project structure and code you can keep working onFUNDING — Rork recently raised $15M and now sees over 743,000 monthly visits with 85% growthPRICING — Rork is free to start, with paid plans from $25/month and Rork Max at $200/monthCROSS — Rork builds iOS, Android, and web from a single prompt, finished off with a bit of follow-up tweakingRORKMAX — Rork Max generates pure Swift instead of React Native, enabling true native apps across iPhone, iPad, Watch, TV, Vision Pro, and iMessageAPPLE — Rork's 2026 direction has a clear theme of native empowerment across the Apple ecosystemEXPO — Standard builds run on React Native and Expo, so you're left with a real project structure and code you can keep working onFUNDING — Rork recently raised $15M and now sees over 743,000 monthly visits with 85% growthPRICING — Rork is free to start, with paid plans from $25/month and Rork Max at $200/monthCROSS — Rork builds iOS, Android, and web from a single prompt, finished off with a bit of follow-up tweaking
Articles/Business
Business/2026-05-06Intermediate

Google Play Subscription Offers: Reduce Churn and Maximize Revenue for Your Rork App

A practical guide to using Google Play subscription offers — free trials, introductory pricing, upgrade offers, and win-back offers — to maximize revenue from Android apps built with Rork.

google-play5subscription28offersmonetization47rork58android3

App Store subscription optimization gets most of the attention in the indie developer community, but Google Play has an equally powerful offer system — and most developers aren't using it fully.

Here's a practical breakdown of how to configure Google Play subscription offers and implement them in an Android app built with Rork.

Types of Subscription Offers in Google Play

Google Play Console lets you attach multiple offers to a single subscription product. The main offer types:

Free Trial

The most common form. You can configure 7, 14, or 30-day free trials. One important nuance: Google enforces that the same user can't claim a free trial for the same app multiple times. This is handled by the platform, not your code.

Introductory Price

Offer the first few months at a reduced price before transitioning to the full rate. For example: first month $0.99, then $4.99/month. Conversion rate is typically lower than free trials, but introductory-price subscribers tend to show higher long-term retention — they were willing to pay from the start.

Upgrade Offer

Target users on a free or lower-tier plan with a discounted path to upgrade. Because you're working with existing users, you can be precise about who sees this offer.

Win-back Offer

Discounted pricing specifically for users who previously cancelled. Google Play lets you configure these to automatically target "past subscribers," which removes the need for you to maintain that list yourself. This is consistently the most underutilized offer type among indie developers.

Configuring Offers in Play Console

  1. Play Console → Your app → Monetize → Products → Subscriptions
  2. Select your subscription → Add offer
  3. Set the offer details

Offer ID (choose carefully): This is what you reference in code. It can't be changed after creation, so establish a naming convention upfront:

  • trial-7days — 7-day free trial
  • intro-1month-half — 50% off first month
  • winback-30pct — 30% off for returning subscribers

Eligibility criteria: Who can see this offer:

  • New subscribers only (never purchased)
  • Existing subscribers (for upgrade offers)
  • Lapsed subscribers (for win-back offers)

Android Implementation (Kotlin, Billing Library v7)

// BillingManager.kt
import com.android.billingclient.api.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
 
class BillingManager(private val activity: Activity) {
 
    private lateinit var billingClient: BillingClient
    
    private val _subscriptionOffers = MutableStateFlow<List<ProductDetails.SubscriptionOfferDetails>>(emptyList())
    val subscriptionOffers: StateFlow<List<ProductDetails.SubscriptionOfferDetails>> = _subscriptionOffers
 
    fun initialize() {
        billingClient = BillingClient.newBuilder(activity)
            .setListener { billingResult, purchases ->
                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    purchases?.forEach { handlePurchase(it) }
                }
            }
            .enablePendingPurchases()
            .build()
 
        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(result: BillingResult) {
                if (result.responseCode == BillingClient.BillingResponseCode.OK) {
                    loadSubscriptionOffers()
                }
            }
            override fun onBillingServiceDisconnected() {
                // Implement reconnection logic
            }
        })
    }
 
    private fun loadSubscriptionOffers() {
        val queryParams = QueryProductDetailsParams.newBuilder()
            .setProductList(
                listOf(
                    QueryProductDetailsParams.Product.newBuilder()
                        .setProductId("premium_monthly")
                        .setProductType(BillingClient.ProductType.SUBS)
                        .build()
                )
            )
            .build()
 
        billingClient.queryProductDetailsAsync(queryParams) { result, productDetailsList ->
            if (result.responseCode == BillingClient.BillingResponseCode.OK) {
                val product = productDetailsList.firstOrNull() ?: return@queryProductDetailsAsync
                val offers = product.subscriptionOfferDetails ?: emptyList()
 
                // Debug: log all available offers
                offers.forEach { offer ->
                    println("Offer ID: ${offer.offerId}")
                    println("Tags: ${offer.offerTags}")
                    offer.pricingPhases.pricingPhaseList.forEach { phase ->
                        println("  Phase: ${phase.formattedPrice} x ${phase.billingCycleCount} cycles")
                    }
                }
 
                _subscriptionOffers.value = offers
            }
        }
    }
 
    fun selectBestOfferForUser(
        offers: List<ProductDetails.SubscriptionOfferDetails>,
        isReturningSubscriber: Boolean,
        hasUsedTrial: Boolean,
    ): ProductDetails.SubscriptionOfferDetails? {
        return when {
            // Returning subscribers get the win-back offer
            isReturningSubscriber -> offers.firstOrNull { it.offerTags.contains("winback") }
            // New users who haven't used a trial get the free trial
            !hasUsedTrial -> offers.firstOrNull { it.offerTags.contains("trial") }
            // Trial already used — offer introductory pricing
            else -> offers.firstOrNull { it.offerTags.contains("introductory") }
        } ?: offers.firstOrNull() // Fallback: base price
    }
 
    fun launchPurchaseFlow(
        productDetails: ProductDetails,
        selectedOffer: ProductDetails.SubscriptionOfferDetails,
    ) {
        val productDetailsParamsList = listOf(
            BillingFlowParams.ProductDetailsParams.newBuilder()
                .setProductDetails(productDetails)
                .setOfferToken(selectedOffer.offerToken) // Critical: pass the offer token
                .build()
        )
 
        val billingFlowParams = BillingFlowParams.newBuilder()
            .setProductDetailsParamsList(productDetailsParamsList)
            .build()
 
        billingClient.launchBillingFlow(activity, billingFlowParams)
    }
 
    private fun handlePurchase(purchase: Purchase) {
        if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
            if (!purchase.isAcknowledged) {
                // Acknowledge within 3 days or Google will refund automatically
                val params = AcknowledgePurchaseParams.newBuilder()
                    .setPurchaseToken(purchase.purchaseToken)
                    .build()
                billingClient.acknowledgePurchase(params) { result ->
                    if (result.responseCode == BillingClient.BillingResponseCode.OK) {
                        println("Acknowledged: ${purchase.orderId}")
                    }
                }
            }
            updateSubscriptionStatus(purchase.purchaseToken)
        }
    }
 
    private fun updateSubscriptionStatus(purchaseToken: String) {
        // Notify RevenueCat or your backend to update subscription status
    }
}

Paywall UI Design for Offers

How you present offers matters as much as the offer itself.

For free trials, show the timeline. "7 days free" is okay. "Start today — cancel anytime before [date] and you won't be charged. After that, $4.99/month" is better. Showing the exact future charge date and making the cancel path clear actually increases conversion because it reduces anxiety about being caught off guard.

For win-back offers, add context. A plain discount ("30% off") feels transactional. "Welcome back — we've held this offer for past subscribers" feels like you thought about them specifically. The latter tends to convert better, particularly in categories where user trust matters.

Measure LTV, not just conversion. When A/B testing offers with RevenueCat Experiments, the headline metric shouldn't be conversion rate — it should be revenue per install at 90 or 180 days. Introductory-price subscribers often show higher LTV than free-trial subscribers, even though the trial converts at a higher initial rate.

Where to Start

If you're not using any offers today, the minimum viable setup is two offers:

  1. 7-day free trial targeting new users
  2. Win-back offer at 30% off targeting lapsed subscribers

Set these up in Play Console, tag them with trial and winback in your offer configuration, and use the selectBestOfferForUser() function above to serve the right offer to the right user. That's the 80/20 version — simple enough to ship in a day, and it covers the two highest-impact scenarios.

For subscription analytics and deeper revenue optimization, the premium article Building a Complete Stripe SaaS Payment System with Rork covers the infrastructure side for web-based monetization alongside your mobile IAP.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Business2026-04-28
Choosing a Revenue Model for Your Rork App — Ads, Subscriptions, or One-Time Purchase, Backed by Real Operating Numbers
Drawing on the period when my wallpaper apps hit peak ad revenue, I rebuilt the comparison of ads, subscriptions, and one-time purchases under identical conditions — with genre-by-genre guidance and implementation notes for Rork.
Business2026-05-06
Building a Complete Stripe SaaS Payment System with Rork: Next.js × Cloudflare Workers × Supabase
A complete guide to implementing Stripe subscriptions, one-time purchases, and webhooks in a Rork-generated Next.js app. Build a production-grade SaaS billing stack with Cloudflare Workers and Supabase from scratch.
Business2026-05-23
From a Rork-Built App to Monthly Break-Even at 100 Yen Subscription: Indie Numbers from May 2026
A small Rork-built utility app reached month-over-month break-even on a 100 yen subscription combined with AdMob. May 2026 numbers, decisions, and what I deliberately left out.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →