Displaying Paywalls
These docs refer to our beta of Paywalls v2. For our original Paywalls, click here.
Platform specific instructionsβ
iOSβ
RevenueCat Paywalls v2 will show paywalls fullscreen, 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
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β
Paywalls v2 supports custom fonts, but it works slightly differently than original Paywalls. The FontProvider
in the SDKs is ignored for v2 Paywalls. Instead, every text component in the paywall editor has a "Font family" property with a gear icon next to it. Clicking the gear icon allows you to configure your font families. In the modal that opens, you can name your font family, and set up the corresponding font for both Android and iOS. Fonts you can use include:
- System fonts
- Any of
sans-serif
,serif
, ormonospace
- Custom fonts already included in your app
Including custom fonts in your appβ
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 original Paywallsβ
Footer Paywallsβ
Paywalls v2 does not support footer Paywalls. If your app requests the Paywall for an Offering to display that has a v2 Paywall, it will display a default version of that paywall instead (see below). Footer mode can still be used on original Paywalls templates using the existing method, or the new .originalTemplatePaywallFooter()
method on iOS SDK versions 5.16.0+.
Close buttonsβ
Paywalls v2 does 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β
Paywalls v2 does not support passing in a custom font provider as original 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 Paywalls v2. See the Custom fonts section for more information.
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 original Paywall templates, consider using Targeting to create an audience that only receives your v2 Paywall if they're using an SDK version that does not support Paywalls v2. 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 v2 Paywall. Learn more about Targeting.