Skip to content

Connect a Compose paywall to RevenueCat

Short answer: fetch the current Offering, map each Package to a plan (package identifier as the id, product.price.formatted as the price), then call purchaseWith with the matching package from onPurchase, and restorePurchasesWith from onRestore.

implementation("com.revenuecat.purchases:purchases:10.29.0")
  1. Configure the SDK once, for example in Application.onCreate:

    // Read the public SDK key from local.properties through BuildConfig. Never commit it.
    fun configureRevenueCat(context: Context, apiKey: String) {
    Purchases.configure(PurchasesConfiguration.Builder(context, apiKey).build())
    }
  2. Map packages to plans. The package type gives a readable title, and the product’s default option tells you about a free trial:

    fun Package.toPaywallPlan(): PaywallPlan = PaywallPlan(
    id = identifier,
    title = when (packageType) {
    PackageType.ANNUAL -> "Yearly"
    PackageType.MONTHLY -> "Monthly"
    PackageType.LIFETIME -> "Lifetime"
    else -> product.title
    },
    price = product.price.formatted,
    period = product.period?.let { "per ${it.unit.name.lowercase()}" } ?: "one-time",
    trialText = product.defaultOption?.freePhase?.billingPeriod?.let { "${it.value}-${it.unit.word()} free trial" },
    badge = if (packageType == PackageType.ANNUAL) "Best value" else null,
    isSubscription = product.period != null,
    )
    private fun Period.Unit.word(): String = name.lowercase()
  3. Load the current Offering. Keep the packages, because you need them to buy:

    fun loadPlans(onLoaded: (packages: List<Package>, plans: List<PaywallPlan>) -> Unit) {
    Purchases.sharedInstance.getOfferingsWith(
    onError = { error -> println("Offerings failed: ${error.message}") },
    onSuccess = { offerings ->
    val packages = offerings.current?.availablePackages.orEmpty()
    onLoaded(packages, packages.map { it.toPaywallPlan() })
    },
    )
    }
  4. Purchase and restore, then check your entitlement (here called pro):

    fun purchase(activity: Activity, packages: List<Package>, plan: PaywallPlan, onUnlocked: () -> Unit) {
    val pkg = packages.first { it.identifier == plan.id }
    Purchases.sharedInstance.purchaseWith(
    PurchaseParams.Builder(activity, pkg).build(),
    onError = { error, userCancelled -> if (!userCancelled) println(error.message) },
    onSuccess = { _, customerInfo ->
    if (customerInfo.entitlements["pro"]?.isActive == true) onUnlocked()
    },
    )
    }
    fun restore(onUnlocked: () -> Unit) {
    Purchases.sharedInstance.restorePurchasesWith(
    onError = { error -> println(error.message) },
    onSuccess = { customerInfo ->
    if (customerInfo.entitlements["pro"]?.isActive == true) onUnlocked()
    },
    )
    }
  5. Show any of the paywalls with the plans, passing purchase and restore as callbacks. See all designs.

RevenueCat ships its own Compose paywall module, purchases-ui, which renders paywalls designed in the RevenueCat dashboard. The two approaches solve different problems:

RevenueCat Paywalls M3 Expressive Paywall + RevenueCat
Where the design lives RevenueCat dashboard (remote) Your code
Change without an app update Yes No
Material 3 Expressive components Not the focus Yes
Works without RevenueCat No Yes, with any billing source

More detail on the comparison page.

FAQ

Isn't RevenueCat's own paywall enough?

RevenueCat Paywalls (the purchases-ui module) are configured remotely in the RevenueCat dashboard. That is great for changing paywalls without app updates. A code-based paywall like this library suits teams that want the UI in their repository, Material 3 Expressive design, or no dependency on a remote paywall config.

Where does the RevenueCat API key go?

Use the public SDK key from the RevenueCat dashboard, keep it in local.properties and expose it through BuildConfig. Don't commit it to a public repository.

Which RevenueCat SDK version is used here?

com.revenuecat.purchases:purchases 10.29.0. The snippets are compiled against it.

Try it in your app

Add the library (JitPack, minSdk 26), then follow Getting started.

implementation("com.github.sanikantv:m3-expressive-paywall:v1.0.0")
Get started

⭐ If this guide helped, star M3 Expressive Paywall on GitHub. It helps other Compose developers find it.

Built by Sanikant. Set in Google Sans Flex (SIL Open Font License). Not affiliated with Google.