From 2e3f0d42791d5847299368034d5c63b0bf99d8cf Mon Sep 17 00:00:00 2001 From: Daniel Pannasch Date: Fri, 18 Jul 2025 14:32:25 -0400 Subject: [PATCH 1/5] Draft of new displaying paywalls docs --- .../paywalls/displaying-paywalls-current.mdx | 275 ++++++++++++++ docs/tools/paywalls/displaying-paywalls.mdx | 352 +++++++----------- 2 files changed, 416 insertions(+), 211 deletions(-) create mode 100644 docs/tools/paywalls/displaying-paywalls-current.mdx diff --git a/docs/tools/paywalls/displaying-paywalls-current.mdx b/docs/tools/paywalls/displaying-paywalls-current.mdx new file mode 100644 index 00000000..6293f475 --- /dev/null +++ b/docs/tools/paywalls/displaying-paywalls-current.mdx @@ -0,0 +1,275 @@ +--- +title: Displaying Paywalls (current) +slug: displaying-paywalls-current +hidden: false +--- + +## 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` or `PaywallViewController` + +import swift1 from '@site/code_blocks/tools/paywalls_1.swift?raw'; +import swift2 from '@site/code_blocks/tools/paywalls_2.swift?raw'; +import swift3 from '@site/code_blocks/tools/paywalls_3.swift?raw'; +import swift4 from '@site/code_blocks/tools/paywalls_4.swift?raw'; +import objc5 from '@site/code_blocks/tools/paywalls_5.m?raw'; + + + +#### 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. + +![Paywalls on iPad](/docs_images/paywalls/paywalls-on-ipad.jpeg) + +### 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`, or `PaywallActivityLauncher` + +import kotlin1 from '@site/code_blocks/tools/paywalls_1.kt?raw'; +import kotlin2 from '@site/code_blocks/tools/paywalls_2.kt?raw'; +import kotlin3 from '@site/code_blocks/tools/paywalls_3.kt?raw'; +import kotlin4 from '@site/code_blocks/tools/paywalls_4.kt?raw'; +import paywallsActivityJava from '@site/code_blocks/tools/paywalls_activity_java.java?raw'; + + + +### 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 ``: this gives you more flexibility on how the paywall is presented. + +import rn1 from '@site/code_blocks/tools/paywalls_rn_1.ts.txt?raw'; +import rn2 from '@site/code_blocks/tools/paywalls_rn_2.ts.txt?raw'; + + + +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. + +import flutter1 from '@site/code_blocks/tools/paywalls_flutter_1.dart?raw'; +import flutter2 from '@site/code_blocks/tools/paywalls_flutter_2.dart?raw'; + + + +#### 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. + +import kmp1 from "@site/code_blocks/tools/paywalls_kmp_1.kts?raw"; + + + +#### 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 + +### Capacitor + +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. + +import cap1 from '@site/code_blocks/tools/paywalls_cap_1.ts.txt?raw'; + + + +## 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](/tools/paywalls/creating-paywalls/components#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](https://developer.android.com/develop/ui/views/text-and-emoji/fonts-in-xml) 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](https://developer.apple.com/documentation/uikit/adding-a-custom-font-to-your-app) 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](/tools/paywalls/creating-paywalls/components#custom-fonts) + +## 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`. + +:::tip Targeting +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.](/tools/targeting) +::: \ No newline at end of file diff --git a/docs/tools/paywalls/displaying-paywalls.mdx b/docs/tools/paywalls/displaying-paywalls.mdx index c63cdf5f..031a539c 100644 --- a/docs/tools/paywalls/displaying-paywalls.mdx +++ b/docs/tools/paywalls/displaying-paywalls.mdx @@ -1,275 +1,205 @@ --- -title: Displaying Paywalls +title: Displaying Paywalls (new) slug: displaying-paywalls hidden: false --- -## Platform specific instructions +Use RevenueCat Paywalls to easily manage, display, and handle your paywall experiences across platforms. -### iOS +## Key concepts -RevenueCat Paywalls will show paywalls in a sheet or fullscreen on iPhone, and there are multiple ways to do this with SwiftUI and UIKit. +| Concept | Description | +| ------------------- | ----------------------------------------------------------------------------------------------------------------- | +| Paywall display modes | Various ways paywalls can be displayed, including: modal sheets, full-screen views, etc. | +| Callbacks | Events provided by the SDK to handle user interactions and outcomes (purchase completed, canceled, errors, etc.) | +| Restore behavior | Automatic handling of purchase restoration within the paywall interface | +| Error handling | Managing scenarios where paywalls fail to load, purchases fail, or restores encounter issues | -- Depending on an entitlement with `presentPaywallIfNeeded` -- Custom logic with `presentPaywallIfNeeded` -- Manually with `PaywallView` or `PaywallViewController` +## Display methods -import swift1 from '@site/code_blocks/tools/paywalls_1.swift?raw'; -import swift2 from '@site/code_blocks/tools/paywalls_2.swift?raw'; -import swift3 from '@site/code_blocks/tools/paywalls_3.swift?raw'; -import swift4 from '@site/code_blocks/tools/paywalls_4.swift?raw'; -import objc5 from '@site/code_blocks/tools/paywalls_5.m?raw'; +RevenueCat Paywalls can be displayed in several ways to fit your application's UI/UX: - +Modal sheets present the paywall in an overlay that can typically be swiped away by the user unless configured otherwise. -#### 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. +- **iOS (SwiftUI):** -![Paywalls on iPad](/docs_images/paywalls/paywalls-on-ipad.jpeg) +```swift +.presentPaywallIfNeeded(requiredEntitlementIdentifier: "premium") +``` -### Android +- **Android (Jetpack Compose):** -RevenueCat Paywalls will, by default, show paywalls fullscreen and there are multiple ways to do this with `Activity`s and Jetpack Compose. +```kotlin +PaywallDialog.showIfNeeded(requiredEntitlementIdentifier = "premium") +``` -- Depending on an entitlement with `PaywallDialog` -- Custom logic with `PaywallDialog` -- Manually with `Paywall`, `PaywallDialog`, or `PaywallActivityLauncher` +### Full-screen view -import kotlin1 from '@site/code_blocks/tools/paywalls_1.kt?raw'; -import kotlin2 from '@site/code_blocks/tools/paywalls_2.kt?raw'; -import kotlin3 from '@site/code_blocks/tools/paywalls_3.kt?raw'; -import kotlin4 from '@site/code_blocks/tools/paywalls_4.kt?raw'; -import paywallsActivityJava from '@site/code_blocks/tools/paywalls_activity_java.java?raw'; +Full-screen views offer an immersive experience by covering the entire screen. - +```swift +.presentPaywallIfNeeded(requiredEntitlementIdentifier: "premium") + .presentationMode(.fullScreenCover) +``` -### React Native +- **iOS (UIKit):** -There are several ways to present paywalls: +```swift +let paywallViewController = PaywallViewController(offeringIdentifier: "your_offering") +present(paywallViewController, animated: true) +``` -- 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 ``: this gives you more flexibility on how the paywall is presented. +- **Android (Compose):** -import rn1 from '@site/code_blocks/tools/paywalls_rn_1.ts.txt?raw'; -import rn2 from '@site/code_blocks/tools/paywalls_rn_2.ts.txt?raw'; +```kotlin +PaywallActivityLauncher.launch(context, offeringIdentifier = "your_offering") +``` - +### Hard paywall -There are also several listeners that can be used to handle the paywall lifecycle, such as `onPurchaseStarted`, `onPurchaseCompleted`, and `onRestoreStarted`. +Hard paywalls prevent dismissal unless the user makes a purchase or restores access. To present a hard paywall, you must: -#### Listeners +1. Configure your paywall without dismiss or close buttons in the RevenueCat Paywall Editor +2. Present the paywall in a full-screen view (not as a dismissable sheet) -When using `RevenueCatUI.Paywall`, you may use one of the provided listeners to react to user actions. +For example, in SwiftUI: -Available listeners at this time are: +```swift +.presentPaywallIfNeeded(requiredEntitlementIdentifier: "premium") + .presentationMode(.fullScreenCover) +``` -- onPurchaseStarted -- onPurchaseCompleted -- onPurchaseError -- onPurchaseCancelled -- onRestoreStarted -- onRestoreCompleted -- onRestoreError -- onDismiss +### Embedded component -### Flutter +Embed paywalls within an existing screen or navigation stack for more integrated experiences. -There are several ways to present paywalls: +- **React Native:** -- 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. +```jsx + +``` -import flutter1 from '@site/code_blocks/tools/paywalls_flutter_1.dart?raw'; -import flutter2 from '@site/code_blocks/tools/paywalls_flutter_2.dart?raw'; +- **Flutter:** - +```dart +PaywallView(offeringIdentifier: "your_offering") +``` -#### Listeners +- **Kotlin Multiplatform (Compose):** -When using `PaywallView`, you may use one of the provided listeners to react to user actions. -Available listeners at this time are: +```kotlin +Paywall(offeringIdentifier = "your_offering") +``` -- onPurchaseStarted -- onPurchaseCompleted -- onPurchaseError -- onRestoreCompleted -- onRestoreError -- onDismiss +## Handling restore behavior -### Kotlin Multiplatform +RevenueCat Paywalls handle restoring purchases seamlessly. Be sure to include a **Restore Purchases** button in your paywall design via the Paywall Editor to give your customers the ability to restore their purchases. -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. +Restores automatically trigger callbacks (`onRestoreStarted`, `onRestoreCompleted`, `onRestoreError`). [Learn more about callbacks](/tools/paywalls/displaying-paywalls#listening-to-callbacks). -import kmp1 from "@site/code_blocks/tools/paywalls_kmp_1.kts?raw"; +### Automatic dismissal - - -#### 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 +The `presentPaywallIfNeeded` method supports passing an entitlement to achieve automatic dismissal behavior upon successful restoration. If the restored entitlement satisfies the paywall requirement, the paywall will automatically dismiss. -### Capacitor - -There are several ways to present paywalls: +```swift +.presentPaywallIfNeeded(requiredEntitlementIdentifier: "premium") { result in + switch result { + case .restored(let customerInfo): + print("Restored: \(customerInfo)") + default: + break + } +} +``` -- 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. +### Manual dismissal -import cap1 from '@site/code_blocks/tools/paywalls_cap_1.ts.txt?raw'; +For other SDK methods of displaying paywalls, automatic dismissal upon entitlement restoration is not supported. Therefore, you need to listen for changes to `CustomerInfo` to determine if an entitlement has been granted and manually dismiss the paywall in your app's logic. - +Example manual dismissal handling: -## 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. +```swift +Purchases.shared.customerInfoStream.sink { customerInfo in + if customerInfo.entitlements["premium"]?.isActive == true { + dismissPaywall() + } +} +``` -## Custom fonts - -Using custom fonts in your paywall can now be done by uploading font files directly to RevenueCat. See the [Custom fonts](/tools/paywalls/creating-paywalls/components#custom-fonts) section for more information. +## Paywall loading -### Including custom fonts in your app +Ensuring your paywalls load quickly provides a better user experience and reduces friction during the purchasing process. Follow these best practices: -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. +- **Pre-fetch Offerings:** Load your offerings early in your app lifecycle to ensure that paywall data is readily available when needed. -#### 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](https://developer.android.com/develop/ui/views/text-and-emoji/fonts-in-xml) for more information. +```swift +Purchases.shared.getOfferings { offerings, error in + if let offerings = offerings { + print("Offerings loaded: \(offerings)") + } +} +``` -#### 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](https://developer.apple.com/documentation/uikit/adding-a-custom-font-to-your-app) for more information. +- **Reduce Asset Sizes:** Optimize and minimize asset sizes (images, fonts) used in your paywall design to ensure faster loading times. -#### 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. +- **Cache Data:** Utilize local caching mechanisms provided by RevenueCat SDKs to minimize network requests and improve loading speed. -## Changes from legacy Paywalls +- **Use SDK Latest Version:** Always keep your RevenueCat SDK up to date to benefit from performance improvements and optimizations. -#### Footer Paywalls +## Handling errors -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. +Manage paywall errors gracefully to ensure smooth user experience: -#### Close buttons +- **Paywall load failure:** Triggered if offerings fail to load. Handle by informing users or retrying. +- **Purchase errors:** Occur during transaction issues or cancellations. Notify users and allow retry. +- **Restore errors:** Handle by displaying an appropriate error message. -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. +Example error handling in React Native: -#### Font provider +```javascript +try { + const result = await RevenueCatUI.presentPaywall(); + if (result === 'ERROR') { + Alert.alert("Error loading paywall", "Please check your connection and try again."); + } +} catch (error) { + console.error(error); +} +``` -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](/tools/paywalls/creating-paywalls/components#custom-fonts) +## Listening to callbacks -## Default Paywall +RevenueCat provides extensive callbacks for user interactions: -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. +| Callback | Description | +| ----------------------- | --------------------------------------------------------------- | +| **onPurchaseStarted** | Triggered when a purchase attempt begins. | +| **onPurchaseCompleted** | Called after successful purchase completion. | +| **onPurchaseCancelled** | Called if user cancels purchase. | +| **onPurchaseError** | Triggered on transaction error. | +| **onRestoreStarted** | Fires when restore begins. | +| **onRestoreCompleted** | Called after successful restoration (even if nothing restored). | +| **onRestoreError** | Triggered if restoration encounters an error. | +| **onDismiss** | Fires when paywall is dismissed. | -The default paywall displays all packages in the Offering. +Example SwiftUI callback implementation: -On iOS it uses the app's `accentColor` for styling. -On Android, it uses the app's `Material3`'s `ColorScheme`. +```swift +RevenueCatUI.presentPaywallIfNeeded(requiredEntitlementIdentifier: "premium") { + purchaseCompleted: { customerInfo in + unlockPremiumFeatures() + }, + restoreCompleted: { customerInfo in + updateUIForRestoredPurchases() + }, + dismiss: { + handlePaywallDismissal() + } +} +``` -:::tip Targeting -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.](/tools/targeting) -::: \ No newline at end of file +[Learn more about creating Paywalls.](/tools/paywalls/creating-paywalls) \ No newline at end of file From 591ef0c92d2d8242f2af9498a9f570ff9bebc9dd Mon Sep 17 00:00:00 2001 From: Daniel Pannasch Date: Fri, 18 Jul 2025 14:49:53 -0400 Subject: [PATCH 2/5] Convert to code blocks and improve snippets based on existing references --- .../tools/paywalls/automatic_dismissal.swift | 17 ++ .../tools/paywalls/callbacks_swiftui.swift | 32 +++ .../tools/paywalls/embedded_flutter.dart | 9 + code_blocks/tools/paywalls/embedded_kmp.kt | 13 ++ .../tools/paywalls/embedded_react_native.tsx | 8 + code_blocks/tools/paywalls/error_handling.js | 8 + .../tools/paywalls/fullscreen_android.kt | 8 + .../tools/paywalls/fullscreen_swiftui.swift | 11 + .../tools/paywalls/fullscreen_uikit.swift | 10 + .../tools/paywalls/hard_paywall_swiftui.swift | 11 + .../tools/paywalls/manual_dismissal.swift | 20 ++ .../tools/paywalls/modal_sheet_android.kt | 17 ++ .../tools/paywalls/modal_sheet_swiftui.swift | 10 + .../tools/paywalls/prefetch_offerings.swift | 12 ++ docs/tools/paywalls/displaying-paywalls.mdx | 199 +++++++++--------- 15 files changed, 288 insertions(+), 97 deletions(-) create mode 100644 code_blocks/tools/paywalls/automatic_dismissal.swift create mode 100644 code_blocks/tools/paywalls/callbacks_swiftui.swift create mode 100644 code_blocks/tools/paywalls/embedded_flutter.dart create mode 100644 code_blocks/tools/paywalls/embedded_kmp.kt create mode 100644 code_blocks/tools/paywalls/embedded_react_native.tsx create mode 100644 code_blocks/tools/paywalls/error_handling.js create mode 100644 code_blocks/tools/paywalls/fullscreen_android.kt create mode 100644 code_blocks/tools/paywalls/fullscreen_swiftui.swift create mode 100644 code_blocks/tools/paywalls/fullscreen_uikit.swift create mode 100644 code_blocks/tools/paywalls/hard_paywall_swiftui.swift create mode 100644 code_blocks/tools/paywalls/manual_dismissal.swift create mode 100644 code_blocks/tools/paywalls/modal_sheet_android.kt create mode 100644 code_blocks/tools/paywalls/modal_sheet_swiftui.swift create mode 100644 code_blocks/tools/paywalls/prefetch_offerings.swift diff --git a/code_blocks/tools/paywalls/automatic_dismissal.swift b/code_blocks/tools/paywalls/automatic_dismissal.swift new file mode 100644 index 00000000..9cd74b18 --- /dev/null +++ b/code_blocks/tools/paywalls/automatic_dismissal.swift @@ -0,0 +1,17 @@ +import SwiftUI +import RevenueCat +import RevenueCatUI + +struct App: View { + var body: some View { + ContentView() + .presentPaywallIfNeeded(requiredEntitlementIdentifier: "premium") { result in + switch result { + case .restored(let customerInfo): + print("Restored: \(customerInfo)") + default: + break + } + } + } +} \ No newline at end of file diff --git a/code_blocks/tools/paywalls/callbacks_swiftui.swift b/code_blocks/tools/paywalls/callbacks_swiftui.swift new file mode 100644 index 00000000..790d6ddd --- /dev/null +++ b/code_blocks/tools/paywalls/callbacks_swiftui.swift @@ -0,0 +1,32 @@ +import SwiftUI +import RevenueCat +import RevenueCatUI + +struct App: View { + var body: some View { + ContentView() + .presentPaywallIfNeeded(requiredEntitlementIdentifier: "premium") { + purchaseCompleted: { customerInfo in + unlockPremiumFeatures() + }, + restoreCompleted: { customerInfo in + updateUIForRestoredPurchases() + }, + dismiss: { + handlePaywallDismissal() + } + } + } + + private func unlockPremiumFeatures() { + // Handle premium features unlock + } + + private func updateUIForRestoredPurchases() { + // Update UI for restored purchases + } + + private func handlePaywallDismissal() { + // Handle paywall dismissal + } +} \ No newline at end of file diff --git a/code_blocks/tools/paywalls/embedded_flutter.dart b/code_blocks/tools/paywalls/embedded_flutter.dart new file mode 100644 index 00000000..6faa95f6 --- /dev/null +++ b/code_blocks/tools/paywalls/embedded_flutter.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; +import 'package:purchases_ui_flutter/purchases_ui_flutter.dart'; + +class MyWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return PaywallView(offeringIdentifier: "your_offering"); + } +} \ No newline at end of file diff --git a/code_blocks/tools/paywalls/embedded_kmp.kt b/code_blocks/tools/paywalls/embedded_kmp.kt new file mode 100644 index 00000000..c6c2e3c8 --- /dev/null +++ b/code_blocks/tools/paywalls/embedded_kmp.kt @@ -0,0 +1,13 @@ +import com.revenuecat.purchases.ui.revenuecatui.Paywall +import com.revenuecat.purchases.ui.revenuecatui.PaywallOptions + +@Composable +fun MyScreen() { + val options = remember { + PaywallOptions(dismissRequest = { TODO("Handle dismiss") }) { + shouldDisplayDismissButton = true + } + } + + Paywall(options) +} \ No newline at end of file diff --git a/code_blocks/tools/paywalls/embedded_react_native.tsx b/code_blocks/tools/paywalls/embedded_react_native.tsx new file mode 100644 index 00000000..0496cdaf --- /dev/null +++ b/code_blocks/tools/paywalls/embedded_react_native.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import RevenueCatUI from 'react-native-purchases-ui'; + +function MyComponent() { + return ( + + ); +} \ No newline at end of file diff --git a/code_blocks/tools/paywalls/error_handling.js b/code_blocks/tools/paywalls/error_handling.js new file mode 100644 index 00000000..2a1773e7 --- /dev/null +++ b/code_blocks/tools/paywalls/error_handling.js @@ -0,0 +1,8 @@ +try { + const result = await RevenueCatUI.presentPaywall(); + if (result === 'ERROR') { + Alert.alert("Error loading paywall", "Please check your connection and try again."); + } +} catch (error) { + console.error(error); +} \ No newline at end of file diff --git a/code_blocks/tools/paywalls/fullscreen_android.kt b/code_blocks/tools/paywalls/fullscreen_android.kt new file mode 100644 index 00000000..d39641c1 --- /dev/null +++ b/code_blocks/tools/paywalls/fullscreen_android.kt @@ -0,0 +1,8 @@ +import android.content.Context +import com.revenuecat.purchases.ui.revenuecatui.PaywallActivityLauncher + +class MainActivity : AppCompatActivity() { + fun presentPaywall(context: Context) { + PaywallActivityLauncher.launch(context, offeringIdentifier = "your_offering") + } +} \ No newline at end of file diff --git a/code_blocks/tools/paywalls/fullscreen_swiftui.swift b/code_blocks/tools/paywalls/fullscreen_swiftui.swift new file mode 100644 index 00000000..68e6c02a --- /dev/null +++ b/code_blocks/tools/paywalls/fullscreen_swiftui.swift @@ -0,0 +1,11 @@ +import SwiftUI +import RevenueCat +import RevenueCatUI + +struct App: View { + var body: some View { + ContentView() + .presentPaywallIfNeeded(requiredEntitlementIdentifier: "premium") + .presentationMode(.fullScreenCover) + } +} \ No newline at end of file diff --git a/code_blocks/tools/paywalls/fullscreen_uikit.swift b/code_blocks/tools/paywalls/fullscreen_uikit.swift new file mode 100644 index 00000000..74de0cce --- /dev/null +++ b/code_blocks/tools/paywalls/fullscreen_uikit.swift @@ -0,0 +1,10 @@ +import UIKit +import RevenueCat +import RevenueCatUI + +class ViewController: UIViewController { + func presentPaywall() { + let paywallViewController = PaywallViewController(offeringIdentifier: "your_offering") + present(paywallViewController, animated: true) + } +} \ No newline at end of file diff --git a/code_blocks/tools/paywalls/hard_paywall_swiftui.swift b/code_blocks/tools/paywalls/hard_paywall_swiftui.swift new file mode 100644 index 00000000..3c5c4926 --- /dev/null +++ b/code_blocks/tools/paywalls/hard_paywall_swiftui.swift @@ -0,0 +1,11 @@ +import SwiftUI +import RevenueCat +import RevenueCatUI + +struct App: View { + var body: some View { + ContentView() + .presentPaywallIfNeeded(requiredEntitlementIdentifier: "premium") + .presentationMode(.fullScreenCover) + } +} diff --git a/code_blocks/tools/paywalls/manual_dismissal.swift b/code_blocks/tools/paywalls/manual_dismissal.swift new file mode 100644 index 00000000..4bf7b626 --- /dev/null +++ b/code_blocks/tools/paywalls/manual_dismissal.swift @@ -0,0 +1,20 @@ +import SwiftUI +import RevenueCat +import Combine + +class PaywallManager: ObservableObject { + private var cancellables = Set() + + func setupCustomerInfoListener() { + Purchases.shared.customerInfoStream.sink { customerInfo in + if customerInfo.entitlements["premium"]?.isActive == true { + dismissPaywall() + } + } + .store(in: &cancellables) + } + + private func dismissPaywall() { + // Handle paywall dismissal + } +} \ No newline at end of file diff --git a/code_blocks/tools/paywalls/modal_sheet_android.kt b/code_blocks/tools/paywalls/modal_sheet_android.kt new file mode 100644 index 00000000..48d1fb16 --- /dev/null +++ b/code_blocks/tools/paywalls/modal_sheet_android.kt @@ -0,0 +1,17 @@ +@OptIn(ExperimentalPreviewRevenueCatUIPurchasesAPI::class) +@Composable +private fun LockedScreen() { + YourContent() + + PaywallDialog( + PaywallDialogOptions.Builder() + .setRequiredEntitlementIdentifier("premium") + .setListener( + object : PaywallListener { + override fun onPurchaseCompleted(customerInfo: CustomerInfo, storeTransaction: StoreTransaction) {} + override fun onRestoreCompleted(customerInfo: CustomerInfo) {} + } + ) + .build() + ) +} \ No newline at end of file diff --git a/code_blocks/tools/paywalls/modal_sheet_swiftui.swift b/code_blocks/tools/paywalls/modal_sheet_swiftui.swift new file mode 100644 index 00000000..9a087d09 --- /dev/null +++ b/code_blocks/tools/paywalls/modal_sheet_swiftui.swift @@ -0,0 +1,10 @@ +import SwiftUI +import RevenueCat +import RevenueCatUI + +struct App: View { + var body: some View { + ContentView() + .presentPaywallIfNeeded(requiredEntitlementIdentifier: "premium") + } +} \ No newline at end of file diff --git a/code_blocks/tools/paywalls/prefetch_offerings.swift b/code_blocks/tools/paywalls/prefetch_offerings.swift new file mode 100644 index 00000000..61341644 --- /dev/null +++ b/code_blocks/tools/paywalls/prefetch_offerings.swift @@ -0,0 +1,12 @@ +import SwiftUI +import RevenueCat + +class OfferingsManager: ObservableObject { + func preloadOfferings() { + Purchases.shared.getOfferings { offerings, error in + if let offerings = offerings { + print("Offerings loaded: \(offerings)") + } + } + } +} \ No newline at end of file diff --git a/docs/tools/paywalls/displaying-paywalls.mdx b/docs/tools/paywalls/displaying-paywalls.mdx index 031a539c..536a1883 100644 --- a/docs/tools/paywalls/displaying-paywalls.mdx +++ b/docs/tools/paywalls/displaying-paywalls.mdx @@ -4,6 +4,21 @@ slug: displaying-paywalls hidden: false --- +import modalSheetSwiftUI from '@site/code_blocks/tools/paywalls/modal_sheet_swiftui.swift?raw'; +import modalSheetAndroid from '@site/code_blocks/tools/paywalls/modal_sheet_android.kt?raw'; +import fullscreenSwiftUI from '@site/code_blocks/tools/paywalls/fullscreen_swiftui.swift?raw'; +import fullscreenUIKit from '@site/code_blocks/tools/paywalls/fullscreen_uikit.swift?raw'; +import fullscreenAndroid from '@site/code_blocks/tools/paywalls/fullscreen_android.kt?raw'; +import hardPaywallSwiftUI from '@site/code_blocks/tools/paywalls/hard_paywall_swiftui.swift?raw'; +import embeddedReactNative from '@site/code_blocks/tools/paywalls/embedded_react_native.tsx?raw'; +import embeddedFlutter from '@site/code_blocks/tools/paywalls/embedded_flutter.dart?raw'; +import embeddedKMP from '@site/code_blocks/tools/paywalls/embedded_kmp.kt?raw'; +import automaticDismissal from '@site/code_blocks/tools/paywalls/automatic_dismissal.swift?raw'; +import manualDismissal from '@site/code_blocks/tools/paywalls/manual_dismissal.swift?raw'; +import prefetchOfferings from '@site/code_blocks/tools/paywalls/prefetch_offerings.swift?raw'; +import errorHandling from '@site/code_blocks/tools/paywalls/error_handling.js?raw'; +import callbacksSwiftUI from '@site/code_blocks/tools/paywalls/callbacks_swiftui.swift?raw'; + Use RevenueCat Paywalls to easily manage, display, and handle your paywall experiences across platforms. ## Key concepts @@ -23,41 +38,40 @@ RevenueCat Paywalls can be displayed in several ways to fit your application's U Modal sheets present the paywall in an overlay that can typically be swiped away by the user unless configured otherwise. -- **iOS (SwiftUI):** - -```swift -.presentPaywallIfNeeded(requiredEntitlementIdentifier: "premium") -``` - -- **Android (Jetpack Compose):** - -```kotlin -PaywallDialog.showIfNeeded(requiredEntitlementIdentifier = "premium") -``` + ### Full-screen view Full-screen views offer an immersive experience by covering the entire screen. -- **iOS (SwiftUI):** - -```swift -.presentPaywallIfNeeded(requiredEntitlementIdentifier: "premium") - .presentationMode(.fullScreenCover) -``` - -- **iOS (UIKit):** - -```swift -let paywallViewController = PaywallViewController(offeringIdentifier: "your_offering") -present(paywallViewController, animated: true) -``` - -- **Android (Compose):** - -```kotlin -PaywallActivityLauncher.launch(context, offeringIdentifier = "your_offering") -``` + ### Hard paywall @@ -68,32 +82,35 @@ Hard paywalls prevent dismissal unless the user makes a purchase or restores acc For example, in SwiftUI: -```swift -.presentPaywallIfNeeded(requiredEntitlementIdentifier: "premium") - .presentationMode(.fullScreenCover) -``` + ### Embedded component Embed paywalls within an existing screen or navigation stack for more integrated experiences. -- **React Native:** - -```jsx - -``` - -- **Flutter:** - -```dart -PaywallView(offeringIdentifier: "your_offering") -``` - -- **Kotlin Multiplatform (Compose):** - -```kotlin -Paywall(offeringIdentifier = "your_offering") -``` + ## Handling restore behavior @@ -105,16 +122,13 @@ Restores automatically trigger callbacks (`onRestoreStarted`, `onRestoreComplete The `presentPaywallIfNeeded` method supports passing an entitlement to achieve automatic dismissal behavior upon successful restoration. If the restored entitlement satisfies the paywall requirement, the paywall will automatically dismiss. -```swift -.presentPaywallIfNeeded(requiredEntitlementIdentifier: "premium") { result in - switch result { - case .restored(let customerInfo): - print("Restored: \(customerInfo)") - default: - break - } -} -``` + ### Manual dismissal @@ -122,13 +136,13 @@ For other SDK methods of displaying paywalls, automatic dismissal upon entitleme Example manual dismissal handling: -```swift -Purchases.shared.customerInfoStream.sink { customerInfo in - if customerInfo.entitlements["premium"]?.isActive == true { - dismissPaywall() - } -} -``` + ## Paywall loading @@ -136,13 +150,13 @@ Ensuring your paywalls load quickly provides a better user experience and reduce - **Pre-fetch Offerings:** Load your offerings early in your app lifecycle to ensure that paywall data is readily available when needed. -```swift -Purchases.shared.getOfferings { offerings, error in - if let offerings = offerings { - print("Offerings loaded: \(offerings)") - } -} -``` + - **Reduce Asset Sizes:** Optimize and minimize asset sizes (images, fonts) used in your paywall design to ensure faster loading times. @@ -160,16 +174,13 @@ Manage paywall errors gracefully to ensure smooth user experience: Example error handling in React Native: -```javascript -try { - const result = await RevenueCatUI.presentPaywall(); - if (result === 'ERROR') { - Alert.alert("Error loading paywall", "Please check your connection and try again."); - } -} catch (error) { - console.error(error); -} -``` + ## Listening to callbacks @@ -188,18 +199,12 @@ RevenueCat provides extensive callbacks for user interactions: Example SwiftUI callback implementation: -```swift -RevenueCatUI.presentPaywallIfNeeded(requiredEntitlementIdentifier: "premium") { - purchaseCompleted: { customerInfo in - unlockPremiumFeatures() - }, - restoreCompleted: { customerInfo in - updateUIForRestoredPurchases() + [Learn more about creating Paywalls.](/tools/paywalls/creating-paywalls) \ No newline at end of file From b532a891b541ec7445eafc7f658a172e6c9773dd Mon Sep 17 00:00:00 2001 From: Daniel Pannasch Date: Fri, 18 Jul 2025 15:40:22 -0400 Subject: [PATCH 3/5] Adding missing sections from existing docs --- docs/tools/paywalls/displaying-paywalls.mdx | 53 +++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/docs/tools/paywalls/displaying-paywalls.mdx b/docs/tools/paywalls/displaying-paywalls.mdx index 536a1883..8e43867d 100644 --- a/docs/tools/paywalls/displaying-paywalls.mdx +++ b/docs/tools/paywalls/displaying-paywalls.mdx @@ -51,6 +51,12 @@ Modal sheets present the paywall in an overlay that can typically be swiped away }, ]}/> +#### 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 methods described below. + +![Paywalls on iPad](/docs_images/paywalls/paywalls-on-ipad.jpeg) + ### Full-screen view Full-screen views offer an immersive experience by covering the entire screen. @@ -207,4 +213,51 @@ Example SwiftUI callback implementation: }, ]}/> +## Custom fonts + +Using custom fonts in your paywall can now be done by uploading font files directly to RevenueCat. See the [Custom fonts](/tools/paywalls/creating-paywalls/components#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 also 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](https://developer.android.com/develop/ui/views/text-and-emoji/fonts-in-xml) 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](https://developer.apple.com/documentation/uikit/adding-a-custom-font-to-your-app) 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](/tools/paywalls/creating-paywalls/components#custom-fonts) + +## 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`. + +:::tip Targeting +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.](/tools/targeting) +::: + [Learn more about creating Paywalls.](/tools/paywalls/creating-paywalls) \ No newline at end of file From 210a4c437f6f37ef248df98c57d84f12d665aacb Mon Sep 17 00:00:00 2001 From: Daniel Pannasch Date: Fri, 18 Jul 2025 16:10:52 -0400 Subject: [PATCH 4/5] Add missing snippets for hybrids --- .../tools/paywalls/fullscreen_flutter.dart | 19 ++++++ code_blocks/tools/paywalls/fullscreen_kmp.kt | 16 +++++ .../paywalls/fullscreen_react_native.tsx | 59 +++++++++++++++++++ .../tools/paywalls/hard_paywall_flutter.dart | 19 ++++++ .../tools/paywalls/hard_paywall_kmp.kt | 16 +++++ .../paywalls/hard_paywall_react_native.tsx | 34 +++++++++++ .../tools/paywalls/modal_sheet_flutter.dart | 21 +++++++ code_blocks/tools/paywalls/modal_sheet_kmp.kt | 15 +++++ .../paywalls/modal_sheet_react_native.tsx | 21 +++++++ docs/tools/paywalls/displaying-paywalls.mdx | 54 +++++++++++++++++ 10 files changed, 274 insertions(+) create mode 100644 code_blocks/tools/paywalls/fullscreen_flutter.dart create mode 100644 code_blocks/tools/paywalls/fullscreen_kmp.kt create mode 100644 code_blocks/tools/paywalls/fullscreen_react_native.tsx create mode 100644 code_blocks/tools/paywalls/hard_paywall_flutter.dart create mode 100644 code_blocks/tools/paywalls/hard_paywall_kmp.kt create mode 100644 code_blocks/tools/paywalls/hard_paywall_react_native.tsx create mode 100644 code_blocks/tools/paywalls/modal_sheet_flutter.dart create mode 100644 code_blocks/tools/paywalls/modal_sheet_kmp.kt create mode 100644 code_blocks/tools/paywalls/modal_sheet_react_native.tsx diff --git a/code_blocks/tools/paywalls/fullscreen_flutter.dart b/code_blocks/tools/paywalls/fullscreen_flutter.dart new file mode 100644 index 00000000..07d6fce1 --- /dev/null +++ b/code_blocks/tools/paywalls/fullscreen_flutter.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:purchases_ui_flutter/purchases_ui_flutter.dart'; + +class FullscreenPaywall extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: PaywallView( + offering: offering, // Optional Offering object + onDismiss: () { + // Handle fullscreen paywall dismissal + // Navigate away or close the screen + }, + ), + ), + ); + } +} \ No newline at end of file diff --git a/code_blocks/tools/paywalls/fullscreen_kmp.kt b/code_blocks/tools/paywalls/fullscreen_kmp.kt new file mode 100644 index 00000000..83db7c01 --- /dev/null +++ b/code_blocks/tools/paywalls/fullscreen_kmp.kt @@ -0,0 +1,16 @@ +import com.revenuecat.purchases.ui.revenuecatui.Paywall +import com.revenuecat.purchases.ui.revenuecatui.PaywallOptions + +@Composable +fun FullscreenPaywall() { + val options = remember { + PaywallOptions(dismissRequest = { + // Handle fullscreen paywall dismissal + // Navigate away or close the screen + }) { + shouldDisplayDismissButton = false // Fullscreen without dismiss button + } + } + + Paywall(options) +} \ No newline at end of file diff --git a/code_blocks/tools/paywalls/fullscreen_react_native.tsx b/code_blocks/tools/paywalls/fullscreen_react_native.tsx new file mode 100644 index 00000000..c7c0aa00 --- /dev/null +++ b/code_blocks/tools/paywalls/fullscreen_react_native.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { View } from 'react-native'; +import RevenueCatUI, { PAYWALL_RESULT } from 'react-native-purchases-ui'; + +// Method 1: Using presentPaywallIfNeeded with fullScreenCover presentation mode +async function presentFullscreenPaywall() { + const paywallResult: PAYWALL_RESULT = await RevenueCatUI.presentPaywallIfNeeded({ + requiredEntitlementIdentifier: "premium", + presentationMode: "fullScreenCover" + }); + + switch (paywallResult) { + case PAYWALL_RESULT.PURCHASED: + case PAYWALL_RESULT.RESTORED: + console.log('Access granted'); + break; + case PAYWALL_RESULT.CANCELLED: + console.log('User cancelled'); + break; + case PAYWALL_RESULT.ERROR: + console.log('Error occurred'); + break; + } +} + +// Method 2: Using Paywall component in fullscreen container +function FullscreenPaywallComponent() { + return ( + + { + // 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. + }} + /> + + ); +} + +// Method 3: Using Paywall component with specific offering +function FullscreenPaywallWithOffering() { + return ( + + { + // 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. + }} + /> + + ); +} \ No newline at end of file diff --git a/code_blocks/tools/paywalls/hard_paywall_flutter.dart b/code_blocks/tools/paywalls/hard_paywall_flutter.dart new file mode 100644 index 00000000..6311651d --- /dev/null +++ b/code_blocks/tools/paywalls/hard_paywall_flutter.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:purchases_ui_flutter/purchases_ui_flutter.dart'; + +class HardPaywall extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: PaywallView( + offering: offering, // Optional Offering object + onDismiss: () { + // Hard paywall - no dismissal allowed + // Only dismiss on successful purchase or restore + }, + ), + ), + ); + } +} \ No newline at end of file diff --git a/code_blocks/tools/paywalls/hard_paywall_kmp.kt b/code_blocks/tools/paywalls/hard_paywall_kmp.kt new file mode 100644 index 00000000..60ca7772 --- /dev/null +++ b/code_blocks/tools/paywalls/hard_paywall_kmp.kt @@ -0,0 +1,16 @@ +import com.revenuecat.purchases.ui.revenuecatui.Paywall +import com.revenuecat.purchases.ui.revenuecatui.PaywallOptions + +@Composable +fun HardPaywall() { + val options = remember { + PaywallOptions(dismissRequest = { + // Hard paywall - no dismissal allowed + // Only dismiss on successful purchase or restore + }) { + shouldDisplayDismissButton = false // No dismiss button for hard paywall + } + } + + Paywall(options) +} \ No newline at end of file diff --git a/code_blocks/tools/paywalls/hard_paywall_react_native.tsx b/code_blocks/tools/paywalls/hard_paywall_react_native.tsx new file mode 100644 index 00000000..d174c361 --- /dev/null +++ b/code_blocks/tools/paywalls/hard_paywall_react_native.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { View } from 'react-native'; +import RevenueCatUI from 'react-native-purchases-ui'; + +// Display current offering +return ( + + { + // 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. + }} + /> + +); + +// If you need to display a specific offering: +return ( + + { + // 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. + }} + /> + +); \ No newline at end of file diff --git a/code_blocks/tools/paywalls/modal_sheet_flutter.dart b/code_blocks/tools/paywalls/modal_sheet_flutter.dart new file mode 100644 index 00000000..eaeab740 --- /dev/null +++ b/code_blocks/tools/paywalls/modal_sheet_flutter.dart @@ -0,0 +1,21 @@ +import 'dart:async'; +import 'dart:developer'; +import 'package:purchases_ui_flutter/purchases_ui_flutter.dart'; + +Future presentModalPaywall() async { + final paywallResult = await RevenueCatUI.presentPaywallIfNeeded("premium"); + log('Paywall result: $paywallResult'); + + switch (paywallResult) { + case PaywallResult.purchased: + case PaywallResult.restored: + print('Access granted'); + break; + case PaywallResult.cancelled: + print('User cancelled'); + break; + case PaywallResult.error: + print('Error occurred'); + break; + } +} \ No newline at end of file diff --git a/code_blocks/tools/paywalls/modal_sheet_kmp.kt b/code_blocks/tools/paywalls/modal_sheet_kmp.kt new file mode 100644 index 00000000..95f7cb9e --- /dev/null +++ b/code_blocks/tools/paywalls/modal_sheet_kmp.kt @@ -0,0 +1,15 @@ +import com.revenuecat.purchases.ui.revenuecatui.Paywall +import com.revenuecat.purchases.ui.revenuecatui.PaywallOptions + +@Composable +fun ModalPaywall() { + val options = remember { + PaywallOptions(dismissRequest = { + // Handle dismiss - this makes it a modal sheet + }) { + shouldDisplayDismissButton = true + } + } + + Paywall(options) +} \ No newline at end of file diff --git a/code_blocks/tools/paywalls/modal_sheet_react_native.tsx b/code_blocks/tools/paywalls/modal_sheet_react_native.tsx new file mode 100644 index 00000000..b34ca54b --- /dev/null +++ b/code_blocks/tools/paywalls/modal_sheet_react_native.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import RevenueCatUI, { PAYWALL_RESULT } from 'react-native-purchases-ui'; + +async function presentModalPaywall() { + const paywallResult: PAYWALL_RESULT = await RevenueCatUI.presentPaywallIfNeeded({ + requiredEntitlementIdentifier: "premium" + }); + + switch (paywallResult) { + case PAYWALL_RESULT.PURCHASED: + case PAYWALL_RESULT.RESTORED: + console.log('Access granted'); + break; + case PAYWALL_RESULT.CANCELLED: + console.log('User cancelled'); + break; + case PAYWALL_RESULT.ERROR: + console.log('Error occurred'); + break; + } +} \ No newline at end of file diff --git a/docs/tools/paywalls/displaying-paywalls.mdx b/docs/tools/paywalls/displaying-paywalls.mdx index 8e43867d..6bbe233f 100644 --- a/docs/tools/paywalls/displaying-paywalls.mdx +++ b/docs/tools/paywalls/displaying-paywalls.mdx @@ -13,6 +13,15 @@ import hardPaywallSwiftUI from '@site/code_blocks/tools/paywalls/hard_paywall_sw import embeddedReactNative from '@site/code_blocks/tools/paywalls/embedded_react_native.tsx?raw'; import embeddedFlutter from '@site/code_blocks/tools/paywalls/embedded_flutter.dart?raw'; import embeddedKMP from '@site/code_blocks/tools/paywalls/embedded_kmp.kt?raw'; +import modalSheetReactNative from '@site/code_blocks/tools/paywalls/modal_sheet_react_native.tsx?raw'; +import modalSheetFlutter from '@site/code_blocks/tools/paywalls/modal_sheet_flutter.dart?raw'; +import modalSheetKMP from '@site/code_blocks/tools/paywalls/modal_sheet_kmp.kt?raw'; +import fullscreenReactNative from '@site/code_blocks/tools/paywalls/fullscreen_react_native.tsx?raw'; +import fullscreenFlutter from '@site/code_blocks/tools/paywalls/fullscreen_flutter.dart?raw'; +import fullscreenKMP from '@site/code_blocks/tools/paywalls/fullscreen_kmp.kt?raw'; +import hardPaywallReactNative from '@site/code_blocks/tools/paywalls/hard_paywall_react_native.tsx?raw'; +import hardPaywallFlutter from '@site/code_blocks/tools/paywalls/hard_paywall_flutter.dart?raw'; +import hardPaywallKMP from '@site/code_blocks/tools/paywalls/hard_paywall_kmp.kt?raw'; import automaticDismissal from '@site/code_blocks/tools/paywalls/automatic_dismissal.swift?raw'; import manualDismissal from '@site/code_blocks/tools/paywalls/manual_dismissal.swift?raw'; import prefetchOfferings from '@site/code_blocks/tools/paywalls/prefetch_offerings.swift?raw'; @@ -49,6 +58,21 @@ Modal sheets present the paywall in an overlay that can typically be swiped away title: "Android (Jetpack Compose)", content: modalSheetAndroid, }, + { + type: 'jsx', + title: "React Native", + content: modalSheetReactNative, + }, + { + type: 'dart', + title: "Flutter", + content: modalSheetFlutter, + }, + { + type: 'kotlin', + title: "Kotlin Multiplatform", + content: modalSheetKMP, + }, ]}/> #### Paywalls on iPad @@ -77,6 +101,21 @@ Full-screen views offer an immersive experience by covering the entire screen. title: "Android (Compose)", content: fullscreenAndroid, }, + { + type: 'jsx', + title: "React Native", + content: fullscreenReactNative, + }, + { + type: 'dart', + title: "Flutter", + content: fullscreenFlutter, + }, + { + type: 'kotlin', + title: "Kotlin Multiplatform", + content: fullscreenKMP, + }, ]}/> ### Hard paywall @@ -94,6 +133,21 @@ For example, in SwiftUI: title: "SwiftUI", content: hardPaywallSwiftUI, }, + { + type: 'jsx', + title: "React Native", + content: hardPaywallReactNative, + }, + { + type: 'dart', + title: "Flutter", + content: hardPaywallFlutter, + }, + { + type: 'kotlin', + title: "Kotlin Multiplatform", + content: hardPaywallKMP, + }, ]}/> ### Embedded component From f359f76be29181d1b26a5214693bce984920266d Mon Sep 17 00:00:00 2001 From: Daniel Pannasch Date: Fri, 18 Jul 2025 16:22:20 -0400 Subject: [PATCH 5/5] Fix code snippet error --- .../paywalls/hard_paywall_react_native.tsx | 62 ++++++++++--------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/code_blocks/tools/paywalls/hard_paywall_react_native.tsx b/code_blocks/tools/paywalls/hard_paywall_react_native.tsx index d174c361..84466e17 100644 --- a/code_blocks/tools/paywalls/hard_paywall_react_native.tsx +++ b/code_blocks/tools/paywalls/hard_paywall_react_native.tsx @@ -2,33 +2,37 @@ import React from 'react'; import { View } from 'react-native'; import RevenueCatUI from 'react-native-purchases-ui'; -// Display current offering -return ( - - { - // 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. - }} - /> - -); +// Method 1: Hard paywall with current offering +function HardPaywall() { + return ( + + { + // Hard paywall - no dismissal allowed + // Only dismiss on successful purchase or restore + }} + /> + + ); +} -// If you need to display a specific offering: -return ( - - { - // 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. - }} - /> - -); \ No newline at end of file +// Method 2: Hard paywall with specific offering +function HardPaywallWithOffering() { + return ( + + { + // Optional listener. Called when a restore has been completed. + // This may be called even if no entitlements have been granted. + }} + onDismiss={() => { + // Hard paywall - no dismissal allowed + // Only dismiss on successful purchase or restore + }} + /> + + ); +} \ No newline at end of file