
Revenuecat Paywall
- 379 installs
- 55 repo stars
- Updated August 3, 2026
- revenuecat/ai-toolkit
Implement RevenueCat paywalls, offerings, and entitlement checks in iOS or Android apps so subscriptions, trials, and upgrades convert with platform-correct purchase flows.
About
Revenuecat-paywall from revenuecat/ai-toolkit helps mobile developers integrate RevenueCat offerings, packages, and entitlement logic into paywall screens and purchase handlers for iOS and Android, reducing subscription setup errors and speeding monetized app builds.
- SDK setup and offerings wiring
- Entitlement gating patterns
- Trial and upgrade UI flows
- StoreKit and Play Billing alignment
- Faster subscription monetization
Revenuecat Paywall by the numbers
- 379 all-time installs (skills.sh)
- +43 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #339 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/revenuecat/ai-toolkit --skill revenuecat-paywallAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 379 |
|---|---|
| repo stars | ★ 55 |
| Last updated | August 3, 2026 |
| Repository | revenuecat/ai-toolkit ↗ |
What it does
Implement RevenueCat paywalls, offerings, and entitlement checks in iOS or Android apps so subscriptions, trials, and upgrades convert with platform-correct purchase flows.
Files
revenuecat-paywall: display a RevenueCat paywall
Use this skill when the user wants to show a paywall that is built and configured in the RevenueCat dashboard, using the native RevenueCatUI components. This skill does not cover building a custom paywall from scratch. For that, use revenuecat-purchase-flow (when available) and Purchases.getOfferings(…) directly.
Prerequisite: integrate-revenuecat has already run. Purchases.configure(…) must succeed before a paywall can load.
1. Detect the platform
Inspect the working directory and pick the first match, from top to bottom:
1. React Native: package.json has a react-native-purchases entry, or react-native as a dependency. react-native-purchases-ui is the paywall package. Read platforms/react-native.md. If expo is also a dependency, note it as an Expo project. 2. Flutter: pubspec.yaml exists at the project root. The paywall package is purchases_ui_flutter. Read platforms/flutter.md. 3. Kotlin Multiplatform: build.gradle.kts contains a kotlin { … } multiplatform source sets block, or depends on com.revenuecat.purchases:purchases-kmp*. The paywall module is purchases-kmp-ui. Read platforms/kmp.md. 4. Android (native): build.gradle(.kts) applies com.android.application (and is not KMP). The paywall dependency is com.revenuecat.purchases:purchases-ui. Read platforms/android.md. 5. iOS (native): Package.swift, *.xcodeproj, *.xcworkspace, or Podfile at the project root. The paywall product is RevenueCatUI. Read platforms/ios.md.
If several match (e.g. an ios/ folder inside a Flutter project), pick the outermost project, the one that owns the build. If still ambiguous, ask the user which platform they want to configure.
2. Shared concepts (all platforms)
- Paywalls require an Offering with a paywall attached in the RevenueCat dashboard. The SDK pulls offerings via
getOfferings(). If no offering has a paywall configured, RevenueCatUI falls back to a default paywall layout, which is not what you want in production. - Offering vs. entitlement. Users purchase a product through a package in an offering. Access is granted via an entitlement (typically
"premium"or"pro"). Gate premium features on the entitlement, not on the offering. - Three presentation patterns:
- (a) First launch modal for users without the entitlement, typically driven by a "present if needed" helper that checks the entitlement and only shows the paywall when missing.
- (b) Gated premium screen. The user taps a premium feature and the paywall opens before the screen loads.
- (c) Conditional present on a CTA tap, such as an "Upgrade" button in settings.
- RevenueCatUI owns the purchase flow. Do not call
Purchases.purchase(…)manually alongside a RevenueCatUI paywall. The paywall calls it internally. Listen for the dismiss or purchase completed callback to react in app code. - Close button is opt in on most platforms. Pass
displayCloseButton = true(iOS / Flutter / RN) orsetShouldDisplayDismissButton(true)(Android / KMP) when the paywall is presented modally and the user needs a way out. Skip it when presenting behind a sheet with its own grabber, or when wrapping the paywall in a navigation controller. - If the app needs a fully custom UI, do not use this skill. Call
Purchases.getOfferings()and render your own components. RevenueCatUI is only for dashboard templated paywalls.
3. Implementation
Read the platform file that matches detection:
platforms/ios.mdplatforms/android.mdplatforms/kmp.mdplatforms/flutter.mdplatforms/react-native.md
Each platform file is self contained: install command, exact snippet to present the paywall, and the callback shape you listen to.
4. Verify
Do not claim the integration is complete until:
1. The project builds on the target platform. 2. The app launches, the code path that presents the paywall runs, and the paywall UI renders with the template configured in the dashboard (not the default fallback layout). 3. Tapping a package and completing a sandbox purchase dismisses the paywall and fires the purchase completed callback (or, for imperative APIs, resolves with a PURCHASED result). 4. Closing the paywall without purchasing fires the dismiss / cancelled callback.
If the paywall shows the default fallback layout instead of your template, the offering does not have a paywall attached in the dashboard. Fix this in the dashboard, then retry.
revenuecat-paywall: Android (native Kotlin)
Install
Find the latest stable release at <https://github.com/RevenueCat/purchases-android/releases> and substitute that tag for <latest> in the snippet below. If GitHub is unreachable, ask the user for a version to pin or check their existing project files for one.
Add the purchases-ui artifact alongside purchases:
// app/build.gradle.kts
dependencies {
implementation("com.revenuecat.purchases:purchases:<latest>")
implementation("com.revenuecat.purchases:purchases-ui:<latest>")
}purchases-ui depends on Jetpack Compose. If your app is not already a Compose app, enable Compose in the module:
android {
buildFeatures { compose = true }
composeOptions { kotlinCompilerExtensionVersion = "…" }
}Implement
Two APIs exist: the Paywall composable and the PaywallActivityLauncher. Pick one based on how your app is built.
Compose: embed Paywall directly
import androidx.compose.runtime.Composable
import com.revenuecat.purchases.CustomerInfo
import com.revenuecat.purchases.Package
import com.revenuecat.purchases.PurchasesError
import com.revenuecat.purchases.models.StoreTransaction
import com.revenuecat.purchases.ui.revenuecatui.Paywall
import com.revenuecat.purchases.ui.revenuecatui.PaywallListener
import com.revenuecat.purchases.ui.revenuecatui.PaywallOptions
@Composable
fun PremiumUpsell(onDismiss: () -> Unit) {
val options = PaywallOptions.Builder(dismissRequest = onDismiss)
.setShouldDisplayDismissButton(true)
.setListener(object : PaywallListener {
override fun onPurchaseCompleted(
customerInfo: CustomerInfo,
storeTransaction: StoreTransaction,
) {
onDismiss()
}
override fun onPurchaseError(error: PurchasesError) {
// show a toast / log
}
})
.build()
Paywall(options = options)
}PaywallOptions.Builder also has setOffering(offering) if you need to force a specific offering. Without it, the paywall uses Offerings.current.
Activity: launch from any ComponentActivity or Fragment
import androidx.activity.ComponentActivity
import com.revenuecat.purchases.ui.revenuecatui.activity.PaywallActivityLauncher
import com.revenuecat.purchases.ui.revenuecatui.activity.PaywallResult
import com.revenuecat.purchases.ui.revenuecatui.activity.PaywallResultHandler
class MainActivity : ComponentActivity() {
private val paywallLauncher = PaywallActivityLauncher(
resultCaller = this,
resultHandler = PaywallResultHandler { result ->
when (result) {
is PaywallResult.Purchased -> { /* entitlement granted */ }
is PaywallResult.Cancelled -> { /* user dismissed */ }
is PaywallResult.Restored -> { /* restore succeeded */ }
is PaywallResult.Error -> { /* error */ }
}
},
)
private fun openPaywall() {
paywallLauncher.launch(
offering = null, // null = Offerings.current
shouldDisplayDismissButton = true,
)
}
}PaywallActivityLauncher must be instantiated during the host Activity or Fragment's onCreate. Calling launch(…) any time after that opens PaywallActivity in full screen.
Notes
PaywallActivityLauncherdoes not support launching from a plainContext. It requires anActivityResultCaller(aComponentActivityorFragment).PaywallListeneron the composable andPaywallResultHandleron the launcher report overlapping events. Pick one path per entry point; do not mix them.setShouldDisplayDismissButton(true)only affects original template paywalls. V2 Paywalls ignore it and render their own dismiss affordance.- If the dashboard offering has no paywall attached,
Paywallrenders a default template. Confirm the offering has a paywall in the RevenueCat dashboard. - Do not call
Purchases.sharedInstance.purchase(…)alongside the paywall. The RevenueCatUI paywall runs the purchase internally.
Verify
Run the app on a device or emulator signed into a Google sandbox tester:
1. Trigger the code path that opens the paywall. The template configured in the dashboard renders. 2. Tap a package and complete a test purchase. The paywall closes and either PaywallListener.onPurchaseCompleted fires (composable) or the result handler receives PaywallResult.Purchased (activity). 3. Dismiss with the close button. dismissRequest (composable) or PaywallResult.Cancelled (activity) fires. 4. Run adb logcat -s Purchases and confirm a successful transaction log line around the purchase.
revenuecat-paywall: Flutter
Paywalls ship in a separate package, purchases_ui_flutter, that must be added alongside purchases_flutter.
Install
Find the latest stable release at <https://github.com/RevenueCat/purchases-flutter/releases> and substitute that tag for <latest> in the snippet below. If GitHub is unreachable, ask the user for a version to pin or check their existing project files for one.
In pubspec.yaml:
dependencies:
purchases_flutter: ^<latest>
purchases_ui_flutter: ^<latest>Then:
flutter pub get
cd ios && pod install && cd ..Minimum iOS deployment target is 13.0 for purchases_flutter, but purchases_ui_flutter uses RevenueCatUI under the hood which needs iOS 15. Update ios/Podfile:
platform :ios, '15.0'Android minimum SDK is 21. purchases_ui_flutter uses Jetpack Compose on Android; the Flutter embedding wraps that for you.
Implement
Two APIs are available: the imperative RevenueCatUI.presentPaywall(…) method and the declarative PaywallView widget. Prefer the imperative one when the paywall is a one shot modal. Use the widget when you want to embed the paywall inside a Flutter route.
Imperative: presentPaywall
import 'package:purchases_flutter/purchases_flutter.dart';
import 'package:purchases_ui_flutter/purchases_ui_flutter.dart';
Future<void> openPaywall() async {
final result = await RevenueCatUI.presentPaywall(
displayCloseButton: true,
);
switch (result) {
case PaywallResult.purchased:
case PaywallResult.restored:
// entitlement granted
break;
case PaywallResult.cancelled:
// user dismissed
break;
case PaywallResult.error:
case PaywallResult.notPresented:
break;
}
}Imperative: presentPaywallIfNeeded
Gate a flow on an entitlement. The SDK checks customerInfo first and only shows the paywall if the entitlement is not active:
final result = await RevenueCatUI.presentPaywallIfNeeded(
'premium',
displayCloseButton: true,
);
if (result == PaywallResult.purchased || result == PaywallResult.notPresented) {
// user has access
}Declarative: PaywallView widget
import 'package:flutter/material.dart';
import 'package:purchases_ui_flutter/purchases_ui_flutter.dart';
class PremiumPage extends StatelessWidget {
const PremiumPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: PaywallView(
displayCloseButton: true,
onPurchaseCompleted: (customerInfo, storeTransaction) {
Navigator.of(context).pop(true);
},
onDismiss: () => Navigator.of(context).pop(false),
onPurchaseError: (error) {
// surface the error
},
),
);
}
}To target a specific offering, pass it via the offering: parameter (both methods and the widget accept an Offering pulled from Purchases.getOfferings()).
Notes
purchases_ui_fluttersupports iOS and Android only. Web, macOS, Windows, and Linux targets are not supported.- Do not call
Purchases.purchasePackage(…)inside the widget callbacks or around the imperative method. The paywall runs the purchase itself. displayCloseButton: trueonly affects original template paywalls. V2 Paywalls render their own close affordance.presentPaywallreturns aPaywallResultenum.notPresentedonly appears forpresentPaywallIfNeededwhen the entitlement is already active.PaywallViewis aStatelessWidgetthat hosts a native platform view. Do not wrap it in a container that constrains its height to zero; it needs to fill available space.
Verify
Run on a device or simulator with a sandbox account configured:
1. Trigger the paywall flow. The dashboard configured template renders. 2. Purchase a package in the sandbox. Either the future returned by presentPaywall resolves to PaywallResult.purchased, or PaywallView.onPurchaseCompleted fires with a non-null CustomerInfo. 3. Close without purchasing. The future resolves to PaywallResult.cancelled or onDismiss fires. 4. After a successful purchase, call Purchases.getCustomerInfo() and confirm customerInfo.entitlements.active['premium'] exists.
revenuecat-paywall: iOS (native)
Install
Find the latest stable release at <https://github.com/RevenueCat/purchases-ios/releases> and substitute that tag for <latest> in the snippet below. If GitHub is unreachable, ask the user for a version to pin or check their existing project files for one.
RevenueCatUI ships in the same repo as RevenueCat. If the app already has RevenueCat via Swift Package Manager, add the RevenueCatUI product to the app target as well.
Swift Package Manager
In Xcode: target → General → Frameworks, Libraries, and Embedded Content → +, pick the purchases-ios package and add the RevenueCatUI library.
For a Package.swift-based project:
.target(
name: "MyApp",
dependencies: [
.product(name: "RevenueCat", package: "purchases-ios"),
.product(name: "RevenueCatUI", package: "purchases-ios"),
]
)CocoaPods
pod 'RevenueCat'
pod 'RevenueCatUI'Then pod install.
Minimum deployment target for PaywallView is iOS 15.0. RevenueCatUI is unavailable on tvOS.
Implement
SwiftUI: gate a premium screen with .sheet
import SwiftUI
import RevenueCat
import RevenueCatUI
struct PremiumFeatureScreen: View {
@State private var isShowingPaywall = false
@State private var hasEntitlement = false
var body: some View {
Group {
if hasEntitlement {
PremiumContentView()
} else {
Button("Unlock premium") { isShowingPaywall = true }
}
}
.sheet(isPresented: $isShowingPaywall) {
PaywallView(displayCloseButton: true)
}
.task {
let info = try? await Purchases.shared.customerInfo()
hasEntitlement = info?.entitlements["premium"]?.isActive == true
}
}
}PaywallView() with no arguments loads Offerings.current. To present a specific offering, pass it explicitly:
PaywallView(offering: offering, displayCloseButton: true)SwiftUI: present if needed
RevenueCatUI ships a view modifier that checks an entitlement and only presents the paywall if the entitlement is not active:
ContentView()
.presentPaywallIfNeeded(requiredEntitlementIdentifier: "premium")Use the full overload to react to lifecycle events:
ContentView()
.presentPaywallIfNeeded(
requiredEntitlementIdentifier: "premium",
purchaseCompleted: { customerInfo in
// granted
},
onDismiss: {
// user closed without buying
}
)UIKit
import UIKit
import RevenueCatUI
final class SettingsViewController: UIViewController {
@IBAction func openPaywall() {
let paywall = PaywallViewController(displayCloseButton: true)
paywall.delegate = self
present(paywall, animated: true)
}
}
extension SettingsViewController: PaywallViewControllerDelegate {
func paywallViewController(
_ controller: PaywallViewController,
didFinishPurchasingWith customerInfo: CustomerInfo
) {
controller.dismiss(animated: true)
}
}Notes
PaywallViewandPaywallViewControllerrequire iOS 15+. On iOS 13–14, there is no RevenueCatUI paywall; fall back to a custom UI.- Do not call
Purchases.shared.purchase(package:)from inside code that also shows aPaywallView. The paywall runs the purchase itself and will double charge if you wire a second path. displayCloseButtononly affects the "original template" paywalls. V2 Paywalls built in the dashboard render their own close affordance per the template.- To react to specific events (purchase started, restore completed, etc.), attach the view modifiers such as
.onPurchaseCompleted { customerInfo in … }or.onRestoreCompleted { customerInfo in … }directly to thePaywallView.
Verify
Run the app on a device or simulator signed into a sandbox account:
1. Trigger the code path that presents the paywall. The dashboard configured template renders. If you see the default RevenueCat fallback template, the offering in the dashboard has no paywall attached. 2. Tap a package and complete a sandbox purchase. The paywall dismisses and customerInfo.entitlements["premium"].isActive is true on the next Purchases.shared.customerInfo() call. 3. Close the paywall without purchasing. The sheet dismisses and your .onDismiss / delegate method fires.
revenuecat-paywall: Kotlin Multiplatform
purchases-kmp-ui is a Compose Multiplatform wrapper over the native RevenueCatUI paywalls on iOS and Android. The composable is Paywall; the configuration type is PaywallOptions, same as the native Android SDK but in the com.revenuecat.purchases.kmp.ui.revenuecatui package.
Install
Find the latest stable release at <https://github.com/RevenueCat/purchases-kmp/releases> and substitute that tag for <latest> in the snippet below. The KMP tag uses a <wrapper>+<bundled-deps> format (e.g. 2.10.2+17.55.1), where the part before + is the KMP wrapper version and the part after is the bundled purchases-hybrid-common version. Use the full tag string as the artifact version first; if Gradle interprets the + as a wildcard, fall back to the wrapper portion only (e.g. 2.10.2). If GitHub is unreachable, ask the user for a version to pin.
In the shared module's build.gradle.kts, add the UI artifact to commonMain:
kotlin {
// your targets: androidTarget(), iosX64(), iosArm64(), iosSimulatorArm64(), etc.
sourceSets {
commonMain.dependencies {
implementation("com.revenuecat.purchases:purchases-kmp-core:<latest>")
implementation("com.revenuecat.purchases:purchases-kmp-ui:<latest>")
}
}
}Compose Multiplatform must already be set up in the shared module (the org.jetbrains.compose plugin and the compose dependencies). If it is not, Paywall will not compile.
On iOS, the UI artifact bridges through the RevenueCatUI framework. Make sure your Kotlin framework links against it. For setups using the Kotlin CocoaPods plugin, the pod integration handles this automatically. For pure SPM setups, follow the iOS linking section of the purchases-kmp README.
Implement
Shared composable:
import androidx.compose.runtime.Composable
import com.revenuecat.purchases.kmp.CustomerInfo
import com.revenuecat.purchases.kmp.PurchasesError
import com.revenuecat.purchases.kmp.models.StoreTransaction
import com.revenuecat.purchases.kmp.Package
import com.revenuecat.purchases.kmp.ui.revenuecatui.Paywall
import com.revenuecat.purchases.kmp.ui.revenuecatui.PaywallListener
import com.revenuecat.purchases.kmp.ui.revenuecatui.PaywallOptions
@Composable
fun PremiumUpsell(onDismiss: () -> Unit) {
val options = PaywallOptions(dismissRequest = onDismiss) {
shouldDisplayDismissButton = true
listener = object : PaywallListener {
override fun onPurchaseCompleted(
customerInfo: CustomerInfo,
storeTransaction: StoreTransaction,
) {
onDismiss()
}
override fun onPurchaseError(error: PurchasesError) {
// surface the error
}
}
}
Paywall(options)
}The PaywallOptions factory is a DSL builder. The equivalent explicit form is:
val options = PaywallOptions.Builder(dismissRequest = onDismiss)
.apply {
shouldDisplayDismissButton = true
listener = /* … */
}
.build()Set a specific offering via the offering property on the builder. If left null, the paywall loads Offerings.current.
Presenting from platform UI
- Android: host the
Paywallcomposable inside any Compose screen (including anAndroidView-freeComponentActivity.setContent { … }). Navigation and dismissal are handled throughdismissRequest. - iOS: embed the shared
Paywallcomposable inside a Compose MultiplatformUIViewController(e.g.ComposeUIViewController { Paywall(options) }), then present that view controller from your SwiftUI or UIKit host.
Notes
- The
PaywallListenercallbacks,PaywallOptionssurface, and thedismissRequestcontract mirror the native Android SDK. Seepurchases-androiddocs for detailed semantics. - Do not call
Purchases.purchase(…)from outside the paywall. The paywall runs the purchase itself and calls the listener with the result. - The exact group/artifact coordinates for
purchases-kmp-uihave evolved across 1.x releases. If the IDE flags the dependency as unresolved, prefer what shows up in the purchases-kmp README for your installed version over the snippet above. - Compose Multiplatform paywalls require the Compose runtime on both targets. Desktop, web, and other non mobile targets are not supported by the paywall module.
Verify
Run the Android target and the iOS target, each with a sandbox tester account:
1. Trigger the composable on each platform. The dashboard configured paywall renders inside the shared Compose host. 2. Complete a sandbox purchase. The paywall dismisses via dismissRequest and PaywallListener.onPurchaseCompleted fires with a non-null CustomerInfo. 3. Dismiss without purchasing. dismissRequest fires with no listener call. 4. Inspect logs on each platform (Xcode console for iOS, adb logcat -s Purchases for Android) to confirm the underlying native SDK ran the transaction.
revenuecat-paywall: React Native
Paywalls ship in a separate package, react-native-purchases-ui, that must be added alongside react-native-purchases.
Install
The npm install commands below resolve the current latest at install time, so no version pin is needed in this skill. To verify the installed version after install, check package.json. The full release history lives at <https://github.com/RevenueCat/react-native-purchases/releases>.
Bare React Native
npm install react-native-purchases-ui
cd ios && pod install && cd ..Expo
npx expo install react-native-purchases-uireact-native-purchases-ui links native code (RevenueCatUI on iOS, purchases-ui Compose on Android). It will not work in Expo Go. Use a development build via npx expo prebuild (bare) or eas build --profile development.
Deployment targets: iOS 15+, Android minSdk 24 (Compose requirement on the native side).
Implement
Two APIs: the imperative RevenueCatUI.presentPaywall(…) / presentPaywallIfNeeded(…) methods, and the declarative <RevenueCatUI.Paywall /> component. Prefer the imperative one for one shot presentations. Use the component to embed the paywall inside a React Native screen.
Imperative: presentPaywall
import RevenueCatUI, { PAYWALL_RESULT } from 'react-native-purchases-ui';
async function openPaywall() {
const result = await RevenueCatUI.presentPaywall({
displayCloseButton: true,
});
switch (result) {
case PAYWALL_RESULT.PURCHASED:
case PAYWALL_RESULT.RESTORED:
// entitlement granted
break;
case PAYWALL_RESULT.CANCELLED:
// user dismissed
break;
case PAYWALL_RESULT.ERROR:
case PAYWALL_RESULT.NOT_PRESENTED:
break;
}
}Imperative: presentPaywallIfNeeded
const result = await RevenueCatUI.presentPaywallIfNeeded({
requiredEntitlementIdentifier: 'premium',
displayCloseButton: true,
});
if (
result === PAYWALL_RESULT.PURCHASED ||
result === PAYWALL_RESULT.NOT_PRESENTED
) {
// user has access
}Declarative: <RevenueCatUI.Paywall /> component
import { View } from 'react-native';
import RevenueCatUI from 'react-native-purchases-ui';
export function PremiumScreen({ navigation }) {
return (
<View style={{ flex: 1 }}>
<RevenueCatUI.Paywall
options={{ displayCloseButton: true }}
onPurchaseCompleted={({ customerInfo }) => {
navigation.goBack();
}}
onDismiss={() => navigation.goBack()}
onPurchaseError={({ error }) => {
// surface the error
}}
/>
</View>
);
}Target a specific offering by passing options={{ offering, displayCloseButton: true }}, where offering comes from await Purchases.getOfferings().
Notes
react-native-purchases-uiis iOS + Android only. There is no web or desktop target.- Do not call
Purchases.purchasePackage(…)inside the paywall callbacks. The paywall drives the purchase itself. - The component
<RevenueCatUI.Paywall />is a native view host. It needs a non zero size. Wrap it in a<View style={{ flex: 1 }} />or give it an explicit height. displayCloseButtononly affects original template paywalls. V2 Paywalls render their own close button.- The default import is a class (
RevenueCatUI) whose static methods (presentPaywall,presentPaywallIfNeeded,presentCustomerCenter) and static components (Paywall,CustomerCenterView) are used directly.
Verify
Run the app on a device or simulator with a sandbox account:
1. Trigger the paywall code path. The dashboard configured template renders. 2. Purchase a package. Either presentPaywall resolves to PAYWALL_RESULT.PURCHASED or the component's onPurchaseCompleted fires with a non-null customerInfo. 3. Close without purchasing. Either the promise resolves to PAYWALL_RESULT.CANCELLED or onDismiss fires. 4. After a successful purchase, call Purchases.getCustomerInfo() and confirm customerInfo.entitlements.active['premium'] is defined.