RevenueCat paywall offers powerful customization of paywalls that render full native views. However, there might be situations where you want to implement your own custom paywall while still leveraging RevenueCat to manage entitlements, offers and packages. This blog post will guide you through how to effectively run your custom paywall alongside a RevenueCat-powered paywall.

Why run multiple paywalls?

You might be wondering why you’d want to manage multiple paywalls simultaneously. Here are a few scenarios where this approach can be highly beneficial:

  • Transition periods: You might have built a custom paywall before and are interested in progressively moving away from it to RevenueCat paywalls. 
  • A/B testing: You have your existing hard coded paywall, and you’ve now built a new paywall in RevenueCat that you want to test against.
  • Targeted offers: For specific user segments, you might have unique pricing or promotional offers that require a highly customized presentations,
  • Advanced logic: For complex onboarding flows or specific user journeys

Your app decides which paywall UI to show, but all purchases still flow through the RevenueCat SDK. This avoids duplicated logic, pricing mismatches, and entitlement bugs while giving you full control over presentation when needed.

Example end-to-end flow with

At a high level, an app with two different paywall types functions like this:

  1. User reaches a paywall decision point
    • For example: end of onboarding, feature gate, or promotional entry point
  2. App requests Offerings for a specific Placement
    • The Placement represents where the paywall is shown in the user journey
  3. RevenueCat returns the appropriate Offering
    • The returned Offering may vary per user based on targeting rules
  4. App inspects Offering metadata
    • Metadata determines which paywall should be displayed
  5. App chooses a paywall UI
    • RevenueCat paywall UI
    • Or a fully custom paywall UI
  6. User selects a product
    • Product and pricing data always come from RevenueCat
  7. Purchase is initiated via the RevenueCat SDK
    • Identical purchase flow regardless of paywall UI
  8. RevenueCat updates entitlements
    • App unlocks or restricts content based on CustomerInfo

This keeps presentation flexible while keeping the subscription system centralized and consistent. If you make use of RevenueCat’s Experiment feature, you can even test and compare how the different placements and paywalls perform in conversions.

Understanding RevenueCat Paywalls

RevenueCat’s paywalls are designed to simplify the process of displaying your products to users. As outlined in the RevenueCat Paywalls documentation, they allow you to create, manage, and test different paywall designs directly within the RevenueCat dashboard. They handle the fetching of product information and present it in a user-friendly way. All this without having to create new releases of your app when you make changes to your paywall.

For most new apps, implementing your paywall with RevenueCat is the best choice, allowing you to iterate and update your paywall without having to push new releases of your app to stores.

Implementing a custom paywall

As mentioned earlier, there are cases when RevenueCat paywalls might not be enough. One such case could for example a key design element in your app that you want to also show in your paywalls. In a gamified app this could be for example a broken streak visualisation, that you want to surface in the paywall to communicate to the customer that subscription would allow them to amend the broken streak.

When you decide to implement a custom paywall, you’re essentially taking control of the UI/UX. However, you’ll still rely on RevenueCat SDK for the backend operations: fetching product information and making purchases.

Step 1: Fetching Product Information

The first step in your custom paywall implementation is to fetch the product information from RevenueCat. This is crucial for displaying the correct subscription options, prices, and introductory offers to your users. Refer to the Displaying Products documentation for detailed instructions.

In Swift, you’ll typically use the Purchases.shared.getOfferings() method to retrieve your configured offerings. Similarly, on Kotlin, you’ll use Purchases.sharedInstance.getOfferingsWith().

This will provide you with the Offering object, which contains all the necessary product details:

1        Purchases.shared.getOfferings { (offerings, error) in
2            if let packages = offerings?.current?.availablePackages {
3                self.display(packages)
4            }
5        }
1Purchases.sharedInstance.getOfferingsWith({ error ->
2  // An error occurred
3}) { offerings ->
4  offerings.current?.availablePackages?.takeUnless { it.isNullOrEmpty() }?.let {
5    // Display packages for sale
6  }
7}

Step 2: making purchases

Once a user selects a product on your custom paywall, you’ll use RevenueCat’s SDK to initiate the purchase. The Making Purchases documentation provides comprehensive guidance on this.

On iOS, you’ll use Purchases.shared.purchase(package:), and on Android, Purchases.sharedInstance.purchaseWith(). Remember to handle the purchase completion and any potential errors.

1Purchases.shared.purchase(package: package) { (transaction, customerInfo, error, userCancelled) in
2  if customerInfo.entitlements["your_entitlement_id"]?.isActive == true {
3    // Unlock "pro" content              
4  }
5}
1Purchases.sharedInstance.purchaseWith(
2  PurchaseParams.Builder(this, aPackage).build(),
3  onError = { error, userCancelled -> /* No purchase */ },
4  onSuccess = { storeTransaction, customerInfo ->
5    if (customerInfo.entitlements["my_entitlement_identifier"]?.isActive == true) {
6      // Unlock "pro" content
7    }
8  }
9)

Step 3: Entitlement Management

Regardless of whether you use a RevenueCat paywall or a custom one, RevenueCat will handle entitlement management. After a successful purchase, RevenueCat will update the user’s CustomerInfo, which you can then use to grant or restrict access to premium features. In Swift you can do this using the Purchases.shared.customerInfo()

1Purchases.shared.getCustomerInfo { (customerInfo, error) in
2    // access latest customerInfo
3}

In Kotlin

1Purchases.sharedInstance.getCustomerInfoWith(
2    onError = { error -> /* Optional error handling */ },
3    onSuccess = { customerInfo -> /* Access latest customerInfo */ },
4

Conditional display logic for paywalls

You can control the logic for showing different paywalls and offerings using either Placement or Offering metadata. With Placement you can define where each type offering is shown, and with Offering medata you can attach additional information to the offering that is then used in the app to differentiate between showing a custom paywall or a RevenueCat paywall.

Targeting by Placement

Using Targeting by Placement you can define paywalls locations in your app to serve unique Offerings at each paywall location. This could mean for example displaying a different paywall at the following different locations:

  • At the end of onboarding (e.g. onboarding_end)
  • When a customer attempts to use a paywalled feature (e.g. feature_gate)
  • When a sale is running (e.g. sale_offer)

The basis for deciding Placements is to understand how they would compliment your ideal customer journey. If you’re running a sale, you most likely want to show a different offer and copy to onboarded customers, compared to those using the app for first time. Do this by defining a sale_offer placement.

When your app fetches Offerings by Placement, RevenueCat returns the Offering to be displayed for that customer at that Placement, letting you display unique paywalls based on the customer journey. In Swift you can accomplish this in the following way:

1Purchases.shared.getOfferings { offerings, error in
2    if let offering = offerings?.currentOffering(forPlacement: "your_placement_identifier") {
3        // Show paywall
4    } else {
5        // Do nothing or continue on to next view
6    }
7}

Similarly in Kotlin:

1Purchases.sharedInstance.getOfferingsWith({ error ->
2    // An error occurred
3}) { offerings ->
4    offerings.getCurrentOfferingForPlacement("your-placement-identifier")?.let {
5        // show paywall
6    } ?: run {
7        // Do nothing or continue on to next view
8    }
9}

Targeting using Offering metadata

The key to running different types of paywalls and customizing what is shown in your custom paywall is Offering metadata. In the RevenueCat dashboard you can add valid JSON in to the Metadata field, by navigating to an Offering and clicking Edit or Configure metadata (in case you haven’t configured metadata yet):

Metadata that you configure in the dashboard could look like this for example:

1{
2  "custom_paywall": true,
3}

This metadata is attached to the Offering data when you call the SDK to get available Offerings. You can then use it to dynamically make changes to how paywalls are displayed in your app.

Using Offering metadata with the SDK

You can access metadata directly from the Offering object in RevenueCat SDK using the offerings method in Swift

1do {
2    let offerings = try await Purchases.shared.offerings()
3
4    if let offering = offerings.current {
5        let useCustomPaywall = offering.metadata["custom_paywall"] as? Bool
6
7        if useCustomPaywall == true {
8            // Show custom paywall UI
9        } else {
10            // Show RevenueCat Paywalls UI
11        }
12    }
13} catch {
14    // An error occurred
15}

and in Kotlin with the same method:

1Purchases.sharedInstance.getOfferingsWith({ error ->
2    // An error occurred
3}) { offerings ->
4    offerings.current?.let { offering ->
5        val useCustomPaywall = offering.metadata["custom_paywall"] as? Boolean
6
7        if (useCustomPaywall == true) {
8            // Show custom paywall UI
9        } else {
10            // Show RevenueCat Paywalls UI
11        }
12    }


Metadata values are optional by nature, so all access should be built so that your app and paywalls still work even if the values are not defined in the dashboard.

Example Conditional Logic

Now the last remaining step is to use the logic for showing your custom paywall or a RevenueCat paywall:

1func shouldShowCustomPaywall(offering: Offering) -> Bool {
2    offering.getMetadataValue(for: "custom_paywall", default: false)
3}
4
5func showPaywall(for offering: Offering) {
6    if shouldShowCustomPaywall(offering: offering) {
7        // Show your custom paywall UI
8        displayCustomPaywall(offering: offering)
9    } else {
10        // Show RevenueCat's default paywall or a paywall built with RevenueCat's templates
11        displayRevenueCatPaywall(offering: offering)
12    }
13}

and in Kotlin:

1
2fun shouldShowCustomPaywall(offering: Offering): Boolean {
3    return offering.metadata["custom_paywall"] as? Boolean ?: false
4}
5
6fun showPaywall(offering: Offering) {
7    if (shouldShowCustomPaywall(offering)) {
8        // Show your custom paywall UI
9        displayCustomPaywall(offering)
10    } else {
11        // Show RevenueCat's default paywall or a paywall built with RevenueCat's templates
12        displayRevenueCatPaywall(offering)
13    }
14}

Conclusion

Running your own custom paywall alongside a RevenueCat paywall provides flexibility for experimentation and targeted offers. By leveraging RevenueCat for product fetching and purchase management, and your engineering skills for custom UI, you can create a highly optimized and engaging subscription experience for your iOS and Android users. This hybrid approach empowers you to maintain control over the front-end presentation while relying on a robust, battle-tested backend for your subscription infrastructure.