← Files RevenueCatARCHIVED FILE
skills/revenuecat-paywall/platforms/flutter.md
4.18 KB · Oct 4, 2026 · 12:24 UTC
# 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`:
```yaml
dependencies:
purchases_flutter: ^<latest>
purchases_ui_flutter: ^<latest>
```
Then:
```bash
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`:
```ruby
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`
```dart
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:
```dart
final result = await RevenueCatUI.presentPaywallIfNeeded(
'premium',
displayCloseButton: true,
);
if (result == PaywallResult.purchased || result == PaywallResult.notPresented) {
// user has access
}
```
### Declarative: `PaywallView` widget
```dart
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_flutter` supports **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: true` only affects original template paywalls. V2 Paywalls render their own close affordance.
- `presentPaywall` returns a `PaywallResult` enum. `notPresented` only appears for `presentPaywallIfNeeded` when the entitlement is already active.
- `PaywallView` is a `StatelessWidget` that 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.
SHA-256: f12b25302f1d3d543020b241e5b71dce620d77ab35c88717a02d32ed7eafc05d