← Files RevenueCatARCHIVED FILE

skills/revenuecat-purchase-flow/platforms/flutter.md

3.53 KB · Oct 2, 2026 · 00:24 UTC

↓ Download file

# revenuecat-purchase-flow: Flutter

## Fetch offerings

```dart
import 'package:purchases_flutter/purchases_flutter.dart';

Future<List<Package>> currentPackages() async {
  final offerings = await Purchases.getOfferings();
  return offerings.current?.availablePackages ?? const [];
}
```

## Purchase a package

`Purchases.purchasePackage` throws a `PlatformException` on failure. Use `PurchasesErrorHelper.getErrorCode(e)` to detect cancellation.

```dart
import 'package:flutter/services.dart';
import 'package:purchases_flutter/purchases_flutter.dart';

sealed class PurchaseOutcome {}
class Purchased extends PurchaseOutcome {}
class Cancelled extends PurchaseOutcome {}
class Failed extends PurchaseOutcome {
  final Object error;
  Failed(this.error);
}

Future<PurchaseOutcome> buy(Package pkg) async {
  try {
    await Purchases.purchasePackage(pkg);
    // Do not unlock content here. A CustomerInfoUpdateListener flips the
    // gated UI (see revenuecat-entitlements-gate).
    return Purchased();
  } on PlatformException catch (e) {
    final code = PurchasesErrorHelper.getErrorCode(e);
    if (code == PurchasesErrorCode.purchaseCancelledError) {
      return Cancelled();
    }
    return Failed(e);
  }
}
```

## Wire it to a widget

```dart
class BuyButton extends StatefulWidget {
  final Package package;
  const BuyButton(this.package, {super.key});

  @override
  State<BuyButton> createState() => _BuyButtonState();
}

class _BuyButtonState extends State<BuyButton> {
  bool _isBuying = false;

  Future<void> _tap() async {
    setState(() => _isBuying = true);
    try {
      final outcome = await buy(widget.package);
      if (!mounted) return;
      if (outcome is Failed) {
        showDialog<void>(
          context: context,
          builder: (_) => AlertDialog(
            title: const Text('Purchase failed'),
            content: Text(outcome.error.toString()),
            actions: [
              TextButton(
                onPressed: () => Navigator.pop(context),
                child: const Text('OK'),
              ),
            ],
          ),
        );
      }
    } finally {
      if (mounted) setState(() => _isBuying = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: _isBuying ? null : _tap,
      child: Text(widget.package.storeProduct.priceString),
    );
  }
}
```

## Restore purchases

```dart
Future<CustomerInfo?> restore() async {
  try {
    return await Purchases.restorePurchases();
  } on PlatformException {
    return null;
  }
}
```

Expose this from a visible "Restore purchases" button on the paywall and/or settings screen.

## Notes

- `PurchasesErrorHelper.getErrorCode(e)` returns a `PurchasesErrorCode` enum. Compare with `==` against `PurchasesErrorCode.purchaseCancelledError` and friends. This is the supported way and survives plugin version bumps.
- `Purchases.purchase(PurchaseParams.package(pkg))` is the newer overload and accepts promotional offers and other options. `purchasePackage(pkg)` remains the simplest call for the common case.
- Do not read `PlatformException.code` strings directly. The underlying native error strings differ between iOS and Android.

## Verify

1. A sandbox purchase flips the "premium" entitlement to active and the listener notifies the UI.
2. Cancelling the native sheet returns `Cancelled` and no error dialog appears.
3. On a fresh install signed into the same store account, "Restore purchases" re-grants access.
4. `flutter logs` (or `adb logcat` / Xcode console) shows the `Purchases` SDK logs through the transaction.

SHA-256: 1e50b3fe59ebcb2eec303f60d223ad905092aeacd9028429910958c753b94955