Flutter Setup

flutter_inapp_purchase provides in-app purchase support for Flutter apps on iOS, Android, and macOS.

Prerequisites#

  • Flutter with iOS 15.0+ / macOS 14.0+ / Android minSdkVersion 23+ (see the platform sections below)
  • An active Apple Developer account and/or Google Play Developer account
  • Products configured in the store consoles — see iOS Setup and Android Setup
  • A real device for purchase testing — simulators and emulators cannot complete purchases

Installation#

sh
flutter pub add flutter_inapp_purchase

Or add it manually to your pubspec.yaml:

yaml
dependencies:
  flutter_inapp_purchase: ^10.6.0

iOS/macOS Native Dependency Manager#

The Dart install command above is the only package install step for most apps. On Flutter 3.44 and newer, Swift Package Manager is enabled by default, and the Flutter CLI resolves the native OpenIAP dependency automatically when you run or build the app.

Projects that disable Swift Package Manager, or projects using an older Flutter toolchain, continue to use CocoaPods. In that case, run CocoaPods after flutter pub get:

sh
(cd ios && pod install)

# If your app also has a macOS target:
(cd macos && pod install)

Do not add OpenIAP manually to your app's Package.swift or Podfile; the Flutter plugin declares the native dependency.

iOS Configuration#

  • Requires iOS 15.0+
  • Enable In-App Purchase capability in Xcode: Target > Signing & Capabilities > + Capability > In-App Purchase
  • When linking with the iOS 27 SDK, regenerate or migrate an older Runner so its UIApplicationSceneManifest selects FlutterSceneDelegate. See the Xcode 27 UIScene checklist.

Declaring itms-apps in ios/Runner/Info.plist is only needed when your own code checks App Store links before opening them (for example canLaunchUrl from url_launcher) — the plugin itself does not require it:

xml
<key>LSApplicationQueriesSchemes</key>
<array>
    <string>itms-apps</string>
</array>

macOS Configuration#

  • Requires macOS 14.0+
  • Enable In-App Purchase capability in Xcode: Target > Signing & Capabilities > + Capability > In-App Purchase

Android Configuration#

Update your android/app/build.gradle:

groovy
android {
    compileSdkVersion 36

    defaultConfig {
        minSdkVersion 23  // Required minimum
        targetSdkVersion 36

        // Required for v7.1.14+: Select Google Play platform
        missingDimensionStrategy 'platform', 'play'
    }
}

For Kotlin DSL (build.gradle.kts):

kt
android {
    compileSdk = 36

    defaultConfig {
        minSdk = 23  // Required minimum
        targetSdk = 36

        // Required for v7.1.14+: Select Google Play platform
        missingDimensionStrategy("platform", "play")
    }
}

Disable IAP on Android#

If the app uses this package only on iOS or macOS, add the following to android/gradle.properties:

props
openiapPlatform=none

Run flutter clean before rebuilding. This keeps the Android plugin registered with a no-op implementation while excluding OpenIAP Google, Play Billing, Horizon, and Amazon IAP SDK dependencies and the billing manifest entries supplied by them. initConnection() returns false; Android store operations report ErrorCode.IapNotAvailable. Omit the property to keep Google Play as the default.

ProGuard Rules (if using ProGuard)

Add to your android/app/proguard-rules.pro:

# In-App Purchase
-keep class dev.hyo.** { *; }
-keep class com.android.vending.billing.**
-keepattributes *Annotation*

Usage#

The typical flow is initConnection → set up purchaseUpdatedListener and purchaseErrorListener fetchProducts requestPurchase finishTransaction. The snippets below cover each step; the Purchase Guide shows the full flow with receipt validation.

Basic Setup#

dart
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';

class StoreScreen extends StatefulWidget {
  const StoreScreen({super.key});

  @override
  State<StoreScreen> createState() => _StoreScreenState();
}

class _StoreScreenState extends State<StoreScreen> {
  final iap = FlutterInappPurchase.instance;

  StreamSubscription<Purchase>? purchaseSub;
  StreamSubscription<PurchaseError>? errorSub;

  @override
  void initState() {
    super.initState();
    _init();
  }

  Future<void> _init() async {
    // Initialize connection
    final connected = await iap.initConnection();
    if (!connected) throw StateError('Store connection failed');

    // Setup listeners
    purchaseSub = iap.purchaseUpdatedListener.listen((purchase) {
      unawaited(_handlePurchase(purchase));
    });

    errorSub = iap.purchaseErrorListener.listen((error) {
      if (error.code == ErrorCode.UserCancelled) return;
      print('${error.code}: ${error.message}');
    });
  }

  Future<void> _handlePurchase(Purchase purchase) async {
    try {
      // Verify the purchase (server-side), then finish it
      await iap.finishTransaction(
        purchase: purchase,
        isConsumable: false, // true for consumables
      );
    } catch (error) {
      print('Purchase processing failed: $error');
    }
  }

  @override
  void dispose() {
    unawaited(purchaseSub?.cancel().catchError(
      (Object error) => print('Listener cleanup failed: $error'),
    ));
    unawaited(errorSub?.cancel().catchError(
      (Object error) => print('Listener cleanup failed: $error'),
    ));
    unawaited(iap.endConnection().then<void>((ended) {
      if (!ended) print('Store teardown did not complete');
    }).catchError(
      (Object error) => print('Store teardown failed: $error'),
    ));
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return const SizedBox.shrink(); // Your store UI
  }
}

Each call here has a full reference — see initConnection, purchaseUpdatedListener, purchaseErrorListener, finishTransaction, and endConnection for parameters and per-store behavior, and Error Codes for the ErrorCode values.

Fetching Products#

Use explicit type parameters for proper type inference:

dart
// In-app products
final products = await iap.fetchProducts<Product>(
  skus: ['premium', 'coins_100'],
  type: ProductQueryType.InApp,
);

// Subscriptions
final subscriptions = await iap.fetchProducts<ProductSubscription>(
  skus: ['monthly_pro', 'yearly_pro'],
  type: ProductQueryType.Subs,
);

for (final product in products) {
  print('${product.title}: ${product.displayPrice}');
}

See fetchProducts for parameters and per-store behavior.

Making a Purchase#

requestPurchase does not return the purchase. Results arrive on the purchaseUpdatedListener stream you set up in Basic Setup (unlike the callback-style hooks in the React Native SDKs), so make sure both listeners are active before you call it.

dart
// Request purchase (results come through purchaseUpdatedListener)
await iap.requestPurchase(
  RequestPurchaseProps.inApp((
    apple: RequestPurchaseIosProps(sku: 'premium'),
    google: RequestPurchaseAndroidProps(skus: ['premium']),
  )),
);

// Or with subscription offers
await iap.requestPurchase(
  RequestPurchaseProps.subs((
    apple: RequestSubscriptionIosProps(sku: 'monthly_pro'),
    google: RequestSubscriptionAndroidProps(
      skus: ['monthly_pro'],
      subscriptionOffers: [offer],
    ),
  )),
);

Restoring Purchases#

dart
// Get available purchases (active items)
final purchases = await iap.getAvailablePurchases();

// Include expired subscriptions (iOS)
final allPurchases = await iap.getAvailablePurchases(
  onlyIncludeActiveItemsIOS: false,
);

See getAvailablePurchases for parameters and per-store behavior.

Troubleshooting#

Build failed: Could not determine dependencies (v7.1.14+)

If Gradle fails with an error about ambiguous variants (horizonReleaseRuntimeElements / playReleaseRuntimeElements), add missingDimensionStrategy to your build.gradle. See the Android Configuration section above.

Products not found

  • Ensure all agreements are signed in App Store Connect / Google Play Console
  • Verify banking, legal, and tax information is complete and approved
  • Check that bundle ID / package name matches exactly
  • Products must be in "Ready to Submit" status (Apple) or "Active" (Google)
  • Wait 15-30 minutes after creating products before testing

Billing unavailable (Android)

  • Test on a real device, not an emulator
  • Ensure Google Play Store is installed and updated
  • App must be signed with the same certificate uploaded to Play Console

Pending purchases

  • Normal for payment methods requiring additional verification
  • Store pending purchases and check again later
  • Implement proper handling for PurchaseState.Pending

Purchase JSON missing dataAndroid (Flutter 10)

The public Purchase field is dataAndroid. Flutter 10 does not accept the former custom-channel alias. Native adapters, MethodChannel fixtures, and mocks must emit dataAndroid. See Deprecations & 3.0 Migration.

Next Steps#