Back to Blog
AI
15 min read

Building a RevenueCat Paywall in React Native: Design Patterns That Convert

Paweł Karniej·March 2026

Building a RevenueCat Paywall in React Native: Design Patterns That Convert

March 2026

A well-designed paywall is the difference between a 2% and an 8% conversion rate. I have seen this firsthand across my own apps. RevenueCat's Paywall UI (RevenueCatUI) gives you a decent starting point -- you can drop in a pre-built paywall screen in under an hour and start collecting revenue. But if you want real control over messaging, layout, and conversion psychology, custom paywalls built with react-native-purchases convert better because you own every pixel and every word.

This post covers the design patterns, pricing psychology, and implementation details that actually move conversion numbers. Everything here comes from building paywalls for AIVidly (which I sold on Flippa) and for Ship React Native.

Table of Contents

  • RevenueCat Paywall UI vs Custom Paywalls
  • Paywall Design Patterns That Work
  • Implementing a Custom Paywall
  • Pricing Psychology
  • A/B Testing Paywalls with RevenueCat
  • The Credit System Alternative
  • Common Paywall Mistakes
  • When a Simple Paywall Is Enough
  • Ship React Native: Paywall Pre-Built
  • RevenueCat Paywall UI vs Custom Paywalls

    RevenueCat ships a React Native package called react-native-purchases-ui (RevenueCatUI) that gives you ready-made paywall screens. Configure your offerings in the dashboard, pick a template, and present it with a single function call. It handles purchase flows, restore, eligibility checks, and localization.

    The problem is that everyone using RevenueCatUI ends up with paywalls that look roughly the same. Your paywall is the highest-leverage screen in your app for revenue, and a generic template means you are leaving money on the table.

    Custom paywalls built with react-native-purchases take more work, but you control where the paywall appears, what copy it shows, and how plans are presented.

    Here is the comparison:

    | Feature | RevenueCat Paywall UI | Custom Paywall |

    |---|---|---|

    | Setup time | Under 1 hour | 4-8 hours |

    | Design flexibility | Template-based, limited | Full control |

    | Custom copy and messaging | Dashboard text fields only | Anything you want |

    | Animations and transitions | Pre-built only | Custom with Reanimated |

    | A/B testing | Built-in via offerings | Manual or via offerings |

    | Feature gating logic | Basic | Fully custom |

    | Platform-specific design | Automatic | You handle it |

    | Maintenance | RevenueCat maintains | You maintain |

    My recommendation: start with RevenueCatUI if you are validating an idea. Switch to a custom paywall once you have enough users to measure conversion. For AIVidly, switching to a custom paywall with targeted messaging increased trial starts by roughly 40%.

    Paywall Design Patterns That Work

    After studying dozens of top-grossing apps and testing variations in my own, four paywall patterns consistently outperform the rest.

    1. The Comparison Table

    Show weekly, monthly, and yearly plans side by side with clear pricing. Highlight the yearly plan as "Best Value" and display the per-week cost so users can compare directly.

    When to use it: When you offer multiple plan durations and want to anchor users toward the yearly option. Works well for utility apps and productivity tools where users understand they will use the app long-term.

    2. The Feature Gate

    Show the user what they are trying to access, but blur it, lock it, or overlay it with a paywall. The user sees exactly what they are missing. This is far more effective than a generic "Upgrade to Pro" screen because it ties the purchase to a specific desire.

    When to use it: When your app has clear premium features that users discover naturally. AI generation results, export options, advanced filters -- anything where the user has already invested effort and wants to see the outcome.

    3. The Free Trial Pitch

    Center the entire paywall around the trial. The headline is not about features -- it is about risk removal. "Try free for 7 days, cancel anytime" in large text, with a single CTA. Plan details are secondary. The goal is to eliminate purchase anxiety.

    When to use it: When your product needs time to demonstrate value. Fitness apps, habit trackers, learning apps -- anything where the benefit compounds over days. This was the best performer for AIVidly because users needed to generate a few videos before they understood the value.

    4. The Social Proof Wall

    Lead with reviews, ratings, and user counts before showing pricing. "Join 50,000+ creators" followed by 3-4 real App Store reviews, then the pricing section. Social proof reduces skepticism, especially for lesser-known apps.

    When to use it: When you have strong App Store reviews (4.5+ stars) and meaningful download numbers. Do not fabricate social proof -- if you only have 200 users, use a different pattern. But once you have real traction, this pattern builds trust fast.

    Implementing a Custom Paywall

    Here is the core pattern for a custom paywall with react-native-purchases. The flow is: fetch offerings, display packages, handle purchase, check entitlements.

    import React, { useEffect, useState } from "react";
    import { View, Text, TouchableOpacity, ActivityIndicator } from "react-native";
    import Purchases, {
      PurchasesOffering,
      PurchasesPackage,
    } from "react-native-purchases";
    
    export function Paywall({ onSuccess }: { onSuccess: () => void }) {
      const [offering, setOffering] = useState<PurchasesOffering | null>(null);
      const [selectedPackage, setSelectedPackage] =
        useState<PurchasesPackage | null>(null);
      const [loading, setLoading] = useState(false);
    
      useEffect(() => {
        async function fetchOfferings() {
          try {
            const offerings = await Purchases.getOfferings();
            if (offerings.current) {
              setOffering(offerings.current);
              // Pre-select yearly as default (pricing psychology)
              const yearly = offerings.current.annual;
              if (yearly) setSelectedPackage(yearly);
            }
          } catch (e) {
            console.error("Failed to fetch offerings", e);
          }
        }
        fetchOfferings();
      }, []);
    
      async function handlePurchase() {
        if (!selectedPackage) return;
        setLoading(true);
        try {
          const { customerInfo } = await Purchases.purchasePackage(
            selectedPackage
          );
          if (customerInfo.entitlements.active["pro"]) {
            onSuccess();
          }
        } catch (e: any) {
          if (!e.userCancelled) {
            console.error("Purchase failed", e);
          }
        } finally {
          setLoading(false);
        }
      }
    
      async function handleRestore() {
        setLoading(true);
        try {
          const customerInfo = await Purchases.restorePurchases();
          if (customerInfo.entitlements.active["pro"]) {
            onSuccess();
          }
        } catch (e) {
          console.error("Restore failed", e);
        } finally {
          setLoading(false);
        }
      }
    
      if (!offering) return <ActivityIndicator />;
    
      return (
        <View>
          {offering.availablePackages.map((pkg) => (
            <TouchableOpacity
              key={pkg.identifier}
              onPress={() => setSelectedPackage(pkg)}
              style={{
                borderWidth: 2,
                borderColor:
                  selectedPackage?.identifier === pkg.identifier
                    ? "#007AFF"
                    : "#E5E5E5",
                borderRadius: 12,
                padding: 16,
                marginBottom: 12,
              }}
            >
              <Text style={{ fontWeight: "600" }}>
                {pkg.product.title}
              </Text>
              <Text>{pkg.product.priceString}</Text>
            </TouchableOpacity>
          ))}
    
          <TouchableOpacity onPress={handlePurchase} disabled={loading}>
            <Text>
              {loading ? "Processing..." : "Continue"}
            </Text>
          </TouchableOpacity>
    
          <TouchableOpacity onPress={handleRestore}>
            <Text>Restore Purchases</Text>
          </TouchableOpacity>
        </View>
      );
    }

    A few things to note. Offerings are fetched from RevenueCat, so you can change pricing without an app update. The yearly plan is pre-selected as default (more on this in the next section). And restore purchases is always available -- Apple will reject your app without it.

    In a real implementation, you would add styling, animations, and trial eligibility checks. But this is the skeleton every custom paywall is built on.

    Pricing Psychology

    The way you present prices matters more than the actual prices. These four techniques have the biggest impact on conversion.

    Anchor with the yearly plan. Always show the yearly plan first or highlight it as the default selection. When a user sees $79.99/year before they see $9.99/month, the monthly price feels expensive by comparison. The yearly plan becomes the "smart" choice. In AIVidly, pre-selecting the yearly plan increased yearly subscriptions by over 25% compared to pre-selecting monthly.

    Show per-week cost for yearly plans. $79.99/year sounds expensive. $1.54/week sounds like nothing. Always calculate and display the weekly equivalent for your annual plan. Place it right next to the monthly per-week cost so the savings are obvious. "$9.99/week" vs "$1.54/week" is a comparison that sells itself.

    Highlight the savings percentage. "Save 68%" in a badge on the yearly plan gives users a concrete reason to pick it. People are loss-averse -- framing the monthly plan as "losing" 68% in potential savings is more motivating than framing the yearly plan as a "deal." Calculate this accurately from your actual prices.

    Make free trial the default path. If you offer a trial, the CTA should say "Start Free Trial" not "Subscribe." The word "free" reduces friction dramatically. Apple and Google handle the trial-to-paid conversion automatically, and RevenueCat tracks trial conversion rates in its dashboard so you can measure the funnel.

    A/B Testing Paywalls with RevenueCat

    RevenueCat's offerings system doubles as a remote config tool for paywalls. You can run real pricing experiments without deploying code changes.

    Offerings as remote config. Create multiple offerings in the RevenueCat dashboard -- for example, "default" and "experiment_high_price." Each offering contains different packages with different pricing or trial configurations. Your app fetches offerings.current, and RevenueCat decides which offering each user sees based on the experiment you configure.

    Creating experiments. In the RevenueCat dashboard, go to Experiments and create a new one. Set a control (your current offering) and a treatment (the new one). Choose your traffic split -- I recommend 50/50 for faster statistical significance. RevenueCat handles random assignment and tracks all revenue metrics per variant.

    Measuring conversion. RevenueCat tracks initial conversion, trial conversion, revenue per user, and retention. Give experiments at least 2-4 weeks to reach statistical significance.

    Things worth testing: trial length (3 vs 7 vs 14 days), yearly vs monthly as default selection, price points, and paywall copy. In my experience, trial length and default plan selection have the largest impact on revenue.

    The Credit System Alternative

    Subscriptions are not always the right model. For AI-powered apps where each action has a real cost (API calls to OpenAI, image generation, video processing), a credit system often makes more sense.

    The idea is simple: instead of unlimited access for a monthly fee, users buy credits and spend them per action. 10 credits for $4.99, 50 for $19.99, 200 for $49.99. The user pays for what they use, and you do not lose money on heavy users burning through expensive API calls.

    This is exactly what I built for AIVidly. Each video generation cost me roughly $0.15-0.40 in API costs. An unlimited subscription at $9.99/month would have been a disaster if a user generated 100 videos. Credits aligned my costs with my revenue.

    The paywall for a credit system looks different from a subscription paywall. Instead of plan comparison, you show credit bundles with clear per-credit pricing. Larger bundles offer better per-credit rates (same anchoring psychology as yearly plans). Show what each credit buys -- "1 credit = 1 AI video" -- so users understand the value.

    RevenueCat handles credit purchases as consumable in-app purchases. Your backend tracks the credit balance, adds credits on purchase, and deducts them when users perform actions.

    Ship React Native includes both a subscription paywall and a credit system out of the box. The credit system is wired up to Convex for real-time balance tracking, so the user sees their credit count update instantly after a purchase or usage.

    Common Paywall Mistakes

    These are the mistakes I see most often in React Native apps, and a few I have made myself.

    Showing the paywall too early. If a user has not experienced any value, a paywall is just an obstacle. Let them use the core feature at least once. For AIVidly, I let users generate one free video before showing the paywall. That single free generation doubled the conversion rate compared to showing it on first launch.

    Not offering a free trial. Trials work. A 7-day free trial typically converts 2-3x better than a direct purchase for subscription apps. Apple and Google make trials easy to configure, and RevenueCat tracks trial-to-paid conversion automatically. There is almost no reason not to offer one.

    Only one pricing option. A single "Subscribe for $9.99/month" button leaves money on the table. Users who would pay for a year are paying monthly (and churning sooner). Users who find monthly too expensive have no cheaper alternative. Always offer at least two options -- monthly and yearly at minimum.

    Not handling restore purchases. Apple requires a "Restore Purchases" button. If you skip it, your app will be rejected. Beyond the requirement, users switch devices and reinstall apps. A visible restore option saves support tickets.

    Ignoring Android differences. Google Play and the App Store handle subscriptions differently. Test your paywall on both platforms. RevenueCat abstracts most of the backend differences, but UI and legal text requirements vary. I have seen apps that convert well on iOS and terribly on Android because the paywall was only tested on iPhone.

    No loading or error states. If getOfferings() fails, your paywall should not crash or show a blank screen. Always handle loading and error states. Users who see a broken paywall will not come back.

    Hard-coding prices. Never hard-code "$9.99/month" in your paywall text. Always use package.product.priceString from RevenueCat, which returns the localized price. Prices vary by country, and hard-coded prices will be wrong for most of your users.

    When a Simple Paywall Is Enough

    Not every app needs a custom paywall with animations, social proof, and pricing psychology. Sometimes RevenueCatUI is the right call.

    Early-stage apps. If you are still figuring out product-market fit, spend your time on the product, not the paywall. A RevenueCatUI template will collect revenue while you focus on retention and engagement. You can always switch to a custom paywall later.

    Validating willingness to pay. Before investing days in paywall optimization, you need to know if anyone will pay at all. RevenueCatUI gets you to that answer faster. If zero people convert with a decent RevenueCatUI paywall, a custom one will not save you -- the problem is your product or audience.

    Solo developers with limited time. If you are a one-person team shipping your first app, a custom paywall is not where you should spend your energy. Ship with RevenueCatUI, get users, and build a custom paywall when you have data to optimize against.

    Ship React Native: Paywall Pre-Built

    Ship React Native includes a custom paywall screen and a credit system, both wired up and ready to go. The paywall uses the comparison table pattern with pre-selected yearly plan, per-week pricing, savings badges, and trial support. The credit system is connected to Convex for real-time balance tracking and RevenueCat for purchase handling.

    You do not have to build any of this from scratch. Configure your products in App Store Connect and Google Play, add them to RevenueCat, update a few constants in the code, and your monetization is live. The patterns described in this post are already implemented -- you just customize the copy and styling to match your app.


    Related posts: