Displaying Paywalls
Platform specific instructions
iOS
RevenueCat Paywalls will show paywalls in a sheet or fullscreen on iPhone, and there are multiple ways to do this with SwiftUI and UIKit.
- Depending on an entitlement with
presentPaywallIfNeeded
- Custom logic with
presentPaywallIfNeeded
- Manually with
PaywallView
orPaywallViewController
- Entitlement
- Custom Logic
- Manually
- Manually (UIKit)
- Manually (UIKit and Objective-C)
import SwiftUI
import RevenueCat
import RevenueCatUI
struct App: View {
var body: some View {
ContentView()
.presentPaywallIfNeeded(
requiredEntitlementIdentifier: "pro",
purchaseCompleted: { customerInfo in
print("Purchase completed: \(customerInfo.entitlements)")
},
restoreCompleted: { customerInfo in
// Paywall will be dismissed automatically if "pro" is now active.
print("Purchases restored: \(customerInfo.entitlements)")
}
)
}
}
import SwiftUI
import RevenueCat
import RevenueCatUI
struct App: View {
var body: some View {
ContentView()
.presentPaywallIfNeeded { customerInfo in
// Returning `true` will present the paywall
return customerInfo.entitlements.active.keys.contains("pro")
} purchaseCompleted: { customerInfo in
print("Purchase completed: \(customerInfo.entitlements)")
} restoreCompleted: {
// Paywall will be dismissed automatically if "pro" is now active.
print("Purchases restored: \(customerInfo.entitlements)")
}
}
}
import SwiftUI
import RevenueCat
import RevenueCatUI
struct App: View {
@State
var displayPaywall = false
var body: some View {
ContentView()
.sheet(isPresented: self.$displayPaywall) {
PaywallView(displayCloseButton: true)
}
}
}
import UIKit
import RevenueCat
import RevenueCatUI
class ViewController: UIViewController {
@IBAction func presentPaywall() {
let controller = PaywallViewController()
controller.delegate = self
present(controller, animated: true, completion: nil)
}
}
extension ViewController: PaywallViewControllerDelegate {
func paywallViewController(_ controller: PaywallViewController,
didFinishPurchasingWith customerInfo: CustomerInfo) {
}
}
#import "ViewController.h"
@import RevenueCat;
@import RevenueCatUI;
@interface ViewController () <RCPaywallViewControllerDelegate>
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (IBAction)showPaywallTapped:(id)sender {
[RCPurchases.sharedPurchases offeringsWithCompletionHandler:^(RCOfferings * _Nullable offerings, NSError * _Nullable error) {
if (error) {
NSLog(@"Error fetching offerings: %@", error.localizedDescription);
return;
}
RCOffering *offering = offerings.current;
if (offering) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"Current offering identifier: %@", offering.identifier);
RCPaywallViewController *controller = [[RCPaywallViewController alloc] initWithOffering:offering
displayCloseButton:YES
shouldBlockTouchEvents:NO
dismissRequestedHandler:^(RCPaywallViewController * _Nonnull controller) {
NSLog(@"dismiss request!");
[controller dismissViewControllerAnimated:YES completion:nil];
}];
controller.delegate = self;
[self presentViewController:controller animated:YES completion:nil];
});
} else {
NSLog(@"No current offering available");
}
}];
}
#pragma mark - PaywallViewControllerDelegate
- (void)paywallViewController:(RCPaywallViewController *)controller
didFinishPurchasingWithCustomerInfo:(RCCustomerInfo *)customerInfo {
// Handle purchase completion here
}
@end
Paywalls on iPad
When using presentPaywallIfNeeded
to display a paywall on iPad, we'll automatically show a paywall in a modal that is roughly iPhone sized. If instead you prefer to show a paywall that is full screen on iPad, you can use the PaywallView
or PaywallViewController
methods instead.
Android
RevenueCat Paywalls will, by default, show paywalls fullscreen and there are multiple ways to do this with Activity
s and Jetpack Compose.
- Depending on an entitlement with
PaywallDialog
- Custom logic with
PaywallDialog
- Manually with
Paywall
,PaywallDialog
, orPaywallActivityLauncher
- Entitlement
- Custom Logic
- Manually
- Manually (Activity)
- Manually (Activity) - Java
@OptIn(ExperimentalPreviewRevenueCatUIPurchasesAPI::class)
@Composable
private fun LockedScreen() {
YourContent()
PaywallDialog(
PaywallDialogOptions.Builder()
.setRequiredEntitlementIdentifier(Constants.ENTITLEMENT_ID)
.setListener(
object : PaywallListener {
override fun onPurchaseCompleted(customerInfo: CustomerInfo, storeTransaction: StoreTransaction) {}
override fun onRestoreCompleted(customerInfo: CustomerInfo) {}
}
)
.build()
)
}
@OptIn(ExperimentalPreviewRevenueCatUIPurchasesAPI::class)
@Composable
private fun NavGraph(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = Screen.Main.route,
) {
composable(route = Screen.Main.route) {
MainScreen()
PaywallDialog(
PaywallDialogOptions.Builder()
.setShouldDisplayBlock { !it.entitlements.active.isEmpty() }
.setListener(
object : PaywallListener {
override fun onPurchaseCompleted(customerInfo: CustomerInfo, storeTransaction: StoreTransaction) {}
override fun onRestoreCompleted(customerInfo: CustomerInfo) {}
}
)
.build()
)
}
}
}
@OptIn(ExperimentalPreviewRevenueCatUIPurchasesAPI::class)
@Composable
private fun NavGraph(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = Screen.Main.route,
) {
composable(route = Screen.Main.route) {
MainScreen()
}
composable(route = Screen.Paywall.route) {
Paywall(
options = PaywallOptions.Builder(
onDismiss = { navController.popBackStack() }
)
.setListener(
object : PaywallListener {
override fun onPurchaseCompleted(customerInfo: CustomerInfo, storeTransaction: StoreTransaction) {}
override fun onRestoreCompleted(customerInfo: CustomerInfo) {}
}
)
.build()
)
}
}
}
@OptIn(ExperimentalPreviewRevenueCatUIPurchasesAPI::class)
class MainActivity : AppCompatActivity(), PaywallResultHandler {
private lateinit var paywallActivityLauncher: PaywallActivityLauncher
private lateinit var root: View
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
paywallActivityLauncher = PaywallActivityLauncher(this, this)
}
private fun launchPaywallActivity() {
paywallActivityLauncher.launchIfNeeded(requiredEntitlementIdentifier = Constants.ENTITLEMENT_ID)
}
override fun onActivityResult(result: PaywallResult) {}
}
@OptIn(markerClass = ExperimentalPreviewRevenueCatUIPurchasesAPI.class)
public class MainActivity extends AppCompatActivity implements PaywallResultHandler {
private PaywallActivityLauncher launcher;
private static final String requiredEntitlementIdentifier = "MY_ENTITLEMENT_ID";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
launcher = new PaywallActivityLauncher(this, this);
}
private void launchPaywallActivity() {
// This will launch the paywall only if the user doesn't have the given entitlement id active.
launcher.launchIfNeeded(requiredEntitlementIdentifier);
// or if you want to launch it without any conditions
launcher.launch();
}
@Override
public void onActivityResult(PaywallResult result) {
// Handle result
}
}
React Native
There are several ways to present paywalls:
- Using
RevenueCatUI.presentPaywall
: this will display a paywall when invoked. - Using
RevenueCatUI.presentPaywallIfNeeded
: this will present a paywall only if the customer does not have an unlocked entitlement. - Manually presenting
<RevenueCatUI.Paywall>
: this gives you more flexibility on how the paywall is presented.
- RevenueCatUI.presentPaywall
- RevenueCatUI.Paywall
import RevenueCatUI, { PAYWALL_RESULT } from "react-native-purchases-ui";
async function presentPaywall(): Promise<boolean> {
// Present paywall for current offering:
const paywallResult: PAYWALL_RESULT = await RevenueCatUI.presentPaywall();
// or if you need to present a specific offering:
const paywallResult: PAYWALL_RESULT = await RevenueCatUI.presentPaywall({
offering: offering // Optional Offering object obtained through getOfferings
});
switch (paywallResult) {
case PAYWALL_RESULT.NOT_PRESENTED:
case PAYWALL_RESULT.ERROR:
case PAYWALL_RESULT.CANCELLED:
return false;
case PAYWALL_RESULT.PURCHASED:
case PAYWALL_RESULT.RESTORED:
return true;
default:
return false;
}
}
async function presentPaywallIfNeeded() {
// Present paywall for current offering:
const paywallResult: PAYWALL_RESULT = await RevenueCatUI.presentPaywallIfNeeded({
requiredEntitlementIdentifier: "pro"
});
// If you need to present a specific offering:
const paywallResult: PAYWALL_RESULT = await RevenueCatUI.presentPaywallIfNeeded({
offering: offering, // Optional Offering object obtained through getOfferings
requiredEntitlementIdentifier: "pro"
});
}
import React from 'react';
import { View } from 'react-native';
import RevenueCatUI from 'react-native-purchases-ui';
// Display current offering
return (
<View style={{ flex: 1 }}>
<RevenueCatUI.Paywall
onDismiss={() => {
// Dismiss the paywall, i.e. remove the view, navigate to another screen, etc.
// Will be called when the close button is pressed (if enabled) or when a purchase succeeds.
}}
/>
</View>
);
// If you need to display a specific offering:
return (
<View style={{ flex: 1 }}>
<RevenueCatUI.Paywall
options={{
offering: offering // Optional Offering object obtained through getOfferings
}}
onRestoreCompleted={({customerInfo}: { customerInfo: CustomerInfo }) => {
// Optional listener. Called when a restore has been completed.
// This may be called even if no entitlements have been granted.
}
onDismiss={() => {
// Dismiss the paywall, i.e. remove the view, navigate to another screen, etc.
// Will be called when the close button is pressed (if enabled) or when a purchase succeeds.
}}
/>
</View>
);
There are also several listeners that can be used to handle the paywall lifecycle, such as onPurchaseStarted
, onPurchaseCompleted
, and onRestoreStarted
.
Listeners
When using RevenueCatUI.Paywall
, you may use one of the provided listeners to react to user actions.
Available listeners at this time are:
- onPurchaseStarted
- onPurchaseCompleted
- onPurchaseError
- onPurchaseCancelled
- onRestoreStarted
- onRestoreCompleted
- onRestoreError
- onDismiss
Flutter
There are several ways to present paywalls:
- Using
RevenueCatUI.presentPaywall
: this will display a paywall when invoked. - Using
RevenueCatUI.presentPaywallIfNeeded
: this will present a paywall only if the customer does not have an unlocked entitlement. - Manually presenting
PaywallView
: this gives you more flexibility on how the paywall is presented.
- RevenueCatUI.presentPaywall
- PaywallView
import 'dart:async';
import 'dart:developer';
import 'package:purchases_ui_flutter/purchases_ui_flutter.dart';
void presentPaywall() async {
final paywallResult = await RevenueCatUI.presentPaywall();
log('Paywall result: $paywallResult');
}
void presentPaywallIfNeeded() async {
final paywallResult = await RevenueCatUI.presentPaywallIfNeeded("pro");
log('Paywall result: $paywallResult');
}
import 'package:purchases_ui_flutter/purchases_ui_flutter.dart';
// Note: Avoid placing PaywallView inside a modal or bottom sheet (e.g., using showModalBottomSheet).
// Instead, include it directly in your widget.
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: PaywallView(
offering: offering, // Optional Offering object obtained through getOfferings
onRestoreCompleted: (CustomerInfo customerInfo) {
// Optional listener. Called when a restore has been completed.
// This may be called even if no entitlements have been granted.
}
onDismiss: () {
// Dismiss the paywall, i.e. remove the view, navigate to another screen, etc.
// Will be called when the close button is pressed (if enabled) or when a purchase succeeds.
},
),
),
),
);
}
Listeners
When using PaywallView
, you may use one of the provided listeners to react to user actions.
Available listeners at this time are:
- onPurchaseStarted
- onPurchaseCompleted
- onPurchaseError
- onRestoreCompleted
- onRestoreError
- onDismiss
Kotlin Multiplatform
You can present a fullscreen Paywall using the Paywall
composable. You have the flexibility to decide when to call this. You could, for instance, add it to your navigation graph.
- Paywall
val options = remember {
PaywallOptions(dismissRequest = { TODO("Handle dismiss") }) {
shouldDisplayDismissButton = true
}
}
Paywall(options)
Listeners
When using Paywall
, you may use one of the provided listeners to react to user actions.
Available listeners at this time are:
- onPurchaseStarted
- onPurchaseCompleted
- onPurchaseError
- onPurchaseCancelled
- onRestoreStarted
- onRestoreCompleted
- onRestoreError
Handling paywall navigation
When creating a paywall, consider whether it will be presented in a sheet, or as a full screen view. Sheets won't require a dedicated close button. Full screen views should have either a close button (if presented modally) or a back button (if part of a navigation stack or host) unless you intend to provide a hard paywall to your customers that cannot be bypassed.
Custom fonts
Using custom fonts in your paywall can now be done by uploading font files directly to RevenueCat. See the Custom fonts section for more information.
Including custom fonts in your app
To improve the performance and reduce loading times of your paywall using custom fonts, you can add the font to your app's resources using the instructions below.
Android
To add a custom font to your Android app, place the font file in the res/font
folder. Make sure that the filename (without the extension) corresponds to the font name in the paywall editor. See the official Android documentation for more information.
iOS
To add a custom font to your iOS app, go to File and then Add Files to “Your Project Name”. The font file should be a target member of your app, and be registered with iOS by adding the "Fonts provided by the application" key to your Info.plist file. Make sure that the filename (without the extension) corresponds to the font name in the paywall editor. See the official iOS documentation for more information.
Kotlin Multiplatform, React Native, and Flutter
Adding custom fonts to a hybrid app involves adding the font files to the underlying Android and iOS projects following the instructions above.
Changes from legacy Paywalls
Footer Paywalls
Our current Paywalls no longer support footer Paywalls. If your app requests the Paywall for an Offering to display that has a current Paywall, it will display a default version of that paywall instead (see below). Footer mode can still be used on legacy Paywalls templates using the existing method, or the new .originalTemplatePaywallFooter()
method on SDK versions that support our current Paywalls.
Close buttons
Our current Paywalls do not require the displayCloseButton
parameter (or equivalent for other platforms), and it will have no effect if used, since close buttons can be optionally added directly to your paywall as a component if desired.
Font provider
Our current Paywalls do not support passing in a custom font provider as legacy Paywalls did. Instead, you can now configure Paywalls to use the fonts you've already installed in your app directly from the Dashboard. Using the original handler will have no effect on current Paywalls. For more information, click here
Default Paywall
If you attempt to display a Paywall for an Offering that doesn't have one configured, or that has a Paywall configured which is not supported on the installed SDK version, the RevenueCatUI SDK will display a default Paywall.
The default paywall displays all packages in the Offering.
On iOS it uses the app's accentColor
for styling.
On Android, it uses the app's Material3
's ColorScheme
.
If your app supports our legacy Paywall templates, consider using Targeting to create an audience that only receives your new Paywall if they're using an SDK version that does not support our current Paywalls. This will ensure that older app versions continue to receive the Offering and Paywall that they support, while any app versions running a supported RC SDK version receive your new Paywall. Learn more about Targeting.