Skip to main content
Version: 7.0

Frequently Asked Questions

Common questions and answers about flutter_inapp_purchase v7.0, covering implementation, platform differences, best practices, and troubleshooting.

General Questions

What is flutter_inapp_purchase?

flutter_inapp_purchase is a Flutter plugin that provides a unified API for implementing in-app purchases across iOS and Android platforms. It follows the OpenIAP specification and supports:

  • Consumable products (coins, gems, lives)
  • Non-consumable products (premium features, ad removal)
  • Auto-renewable subscriptions
  • Subscription offers and promotional codes
  • Receipt validation
  • Purchase restoration

Which platforms are supported?

Currently supported platforms:

  • iOS (12.0+) - Uses StoreKit 2 (iOS 15.0+) with fallback to StoreKit 1
  • Android (minSdkVersion 21) - Uses Google Play Billing Library v6+

What's new in v7.0?

Major changes in v7.0:

// Named parameters API
final products = await iap.fetchProducts(
skus: ['product_id'],
type: ProductQueryType.InApp,
);

// Simplified finishTransaction
await iap.finishTransaction(
purchase: purchase,
isConsumable: true,
);

Key improvements:

  • Named parameters - All methods now use named parameters for clearer API
  • Simplified finishTransaction - Pass Purchase object directly
  • Better OpenIAP alignment - Closer adherence to OpenIAP specification
  • Removed deprecated iOS methods - Use standard methods instead

See Migration Guide for details.

Implementation Questions

How do I get started?

Basic implementation steps:

// 1. Import the package
import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';

// 2. Initialize connection
final iap = FlutterInappPurchase.instance;
await iap.initConnection();

// 3. Set up listeners
StreamSubscription? _purchaseUpdatedSubscription;
StreamSubscription? _purchaseErrorSubscription;

_purchaseUpdatedSubscription = iap.purchaseUpdatedListener.listen(
(purchase) {
debugPrint('Purchase received: ${purchase.productId}');
_handlePurchase(purchase);
},
);

_purchaseErrorSubscription = iap.purchaseErrorListener.listen(
(error) {
debugPrint('Purchase error: ${error.message}');
_handleError(error);
},
);

// 4. Load products
final products = await iap.fetchProducts(
skus: ['product_id_1', 'product_id_2'],
type: ProductQueryType.InApp,
);

// 5. Request purchase
await iap.requestPurchase(sku: 'product_id');

How do I handle different product types?

final iap = FlutterInappPurchase.instance;

// Consumable products (coins, gems)
await iap.requestPurchase(sku: 'consumable_product');

// In purchase handler:
await iap.finishTransaction(
purchase: purchase,
isConsumable: true, // Consumes on Android, finishes on iOS
);

// Non-consumable products (premium features)
// Check if already owned first
final purchases = await iap.getAvailablePurchases();
final alreadyOwned = purchases.any((p) => p.productId == 'non_consumable');

if (!alreadyOwned) {
await iap.requestPurchase(sku: 'non_consumable');

// In purchase handler:
await iap.finishTransaction(
purchase: purchase,
isConsumable: false, // Acknowledges on Android, finishes on iOS
);
}

// Subscriptions
await iap.requestPurchase(
RequestPurchaseProps.subs(
request: RequestPurchasePropsByPlatforms(
ios: RequestPurchaseIosProps(sku: 'subscription_id'),
android: RequestPurchaseAndroidProps(skus: ['subscription_id']),
),
),
);

How do I restore purchases?

Future<void> restorePurchases() async {
try {
final purchases = await iap.getAvailablePurchases();

if (purchases.isNotEmpty) {
debugPrint('Restored ${purchases.length} purchases');

for (final purchase in purchases) {
// Deliver content for each restored purchase
await deliverContent(purchase.productId);
}
} else {
debugPrint('No purchases to restore');
}
} catch (e) {
debugPrint('Restore failed: $e');
}
}

How do I validate receipts?

Always validate purchases server-side for security:

Future<void> _handlePurchase(Purchase purchase) async {
// 1. Verify on your server
final isValid = await verifyPurchaseOnServer(purchase);

if (!isValid) {
debugPrint('Invalid purchase');
return;
}

// 2. Deliver content
await deliverContent(purchase.productId);

// 3. Finish transaction
await iap.finishTransaction(
purchase: purchase,
isConsumable: true, // or false for non-consumables
);
}

Future<bool> verifyPurchaseOnServer(Purchase purchase) async {
try {
final response = await http.post(
Uri.parse('https://your-server.com/verify-purchase'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'platform': Platform.isIOS ? 'ios' : 'android',
'productId': purchase.productId,
'transactionReceipt': purchase.transactionReceipt, // iOS
'purchaseToken': purchase.purchaseToken, // Android
}),
);

return response.statusCode == 200;
} catch (e) {
debugPrint('Verification failed: $e');
return false;
}
}

Platform Differences

What are the key differences between iOS and Android?

FeatureiOSAndroid
Receipt FormatBase64 encoded receiptPurchase token
Pending PurchasesNot supportedSupported (purchaseStateAndroid = 2)
Offer CodespresentCodeRedemptionSheetIOS()Not supported
Subscription UpgradesAutomatic handlingUse replacementModeAndroid
Transaction FinishingfinishTransaction() finishesConsumes or acknowledges based on isConsumable
Sandbox TestingSandbox accountsTest accounts & license testers

How do I handle platform-specific features?

// iOS-specific: Offer code redemption
if (Platform.isIOS) {
await iap.presentCodeRedemptionSheetIOS();
}

// iOS-specific: Check introductory offer eligibility
if (Platform.isIOS) {
final eligible = await iap.isEligibleForIntroOfferIOS('subscription_id');
debugPrint('Eligible for intro offer: $eligible');
}

// Android-specific: Handle pending purchases
iap.purchaseUpdatedListener.listen((purchase) {
if (Platform.isAndroid && purchase.purchaseStateAndroid == 2) {
debugPrint('Purchase pending: ${purchase.productId}');
// Show pending UI
} else {
_handlePurchase(purchase);
}
});

// Android-specific: Subscription upgrade/downgrade
if (Platform.isAndroid) {
await iap.requestPurchase(
RequestPurchaseProps.subs(
request: RequestPurchasePropsByPlatforms(
android: RequestPurchaseAndroidProps(
skus: ['new_subscription'],
oldSkuAndroid: 'old_subscription',
purchaseTokenAndroid: oldPurchaseToken,
replacementModeAndroid: AndroidReplacementMode.withTimeProration.value,
),
),
),
);
}

Do I need different product IDs for each platform?

Yes, typically you'll have different product IDs configured in App Store Connect and Google Play Console:

class ProductConfig {
// Platform-specific product IDs
static const productIds = {
'premium': Platform.isIOS ? 'com.app.premium.ios' : 'com.app.premium.android',
'coins_100': Platform.isIOS ? 'com.app.coins100.ios' : 'com.app.coins100.android',
};

// Or use a mapping approach
static String getProductId(String key) {
const iosIds = {
'premium': 'com.app.premium.ios',
'coins_100': 'com.app.coins100.ios',
};

const androidIds = {
'premium': 'com.app.premium.android',
'coins_100': 'com.app.coins100.android',
};

return Platform.isIOS ? iosIds[key]! : androidIds[key]!;
}
}

Troubleshooting

Why are my products not loading?

Common causes:

  1. iOS: Products not "Ready to Submit" in App Store Connect
  2. iOS: Banking/tax information incomplete
  3. Android: App not published (even to internal testing)
  4. Android: Signed APK/AAB not uploaded
  5. Both: Product IDs don't match exactly
// Debug product loading
try {
final products = await iap.fetchProducts(
skus: ['your_product_id'],
type: ProductQueryType.InApp,
);

if (products.isEmpty) {
debugPrint('No products loaded - check product IDs and store setup');
} else {
debugPrint('Loaded ${products.length} products');
}
} catch (e) {
debugPrint('Product loading error: $e');
}

Why do purchases fail silently?

Always listen to both purchase streams:

// ✅ Correct: Listen to both streams
_purchaseUpdatedSubscription = iap.purchaseUpdatedListener.listen((purchase) {
debugPrint('Purchase success: ${purchase.productId}');
_handlePurchase(purchase);
});

_purchaseErrorSubscription = iap.purchaseErrorListener.listen((error) {
debugPrint('Purchase error: ${error.code} - ${error.message}');
_handleError(error);
});

// Don't forget to cancel subscriptions

void dispose() {
_purchaseUpdatedSubscription?.cancel();
_purchaseErrorSubscription?.cancel();
super.dispose();
}

I see both success and error for one subscription purchase

This can happen on iOS due to StoreKit 2 event timing. If you already processed a success, you can safely ignore a subsequent transient error:

class PurchaseDeduper {
int _lastSuccessMs = 0;

void setupListeners() {
iap.purchaseUpdatedListener.listen((purchase) async {
_lastSuccessMs = DateTime.now().millisecondsSinceEpoch;
await iap.finishTransaction(purchase: purchase, isConsumable: false);
});

iap.purchaseErrorListener.listen((error) {
// Ignore user cancellation
if (error.code == ErrorCode.userCancelled) return;

// Ignore spurious errors that follow success within 1.5s
final now = DateTime.now().millisecondsSinceEpoch;
final timeSinceSuccess = now - _lastSuccessMs;
if (timeSinceSuccess >= 0 && timeSinceSuccess < 1500) {
debugPrint('Ignoring spurious error after success');
return;
}

// Handle real errors
_handleError(error);
});
}
}

Important: requestPurchase() is event-driven, not promise-based. Don't rely on await requestPurchase() for the final status—handle results via listeners.

How do I handle common error codes?

void _handleError(PurchaseError error) {
switch (error.code) {
case ErrorCode.userCancelled:
// Don't show error - user intentionally cancelled
debugPrint('User cancelled purchase');
break;

case ErrorCode.networkError:
_showMessage('Network error. Please check your connection and try again.');
break;

case ErrorCode.itemAlreadyOwned:
_showMessage('You already own this item.');
// Suggest restore purchases
break;

case ErrorCode.itemUnavailable:
_showMessage('This item is currently unavailable.');
break;

default:
_showMessage('Purchase failed: ${error.message}');
debugPrint('Error code: ${error.code}');
}
}

How do I handle stuck transactions?

Future<void> clearStuckTransactions() async {
final purchases = await iap.getAvailablePurchases();

for (final purchase in purchases) {
// Verify and deliver content
final isValid = await verifyPurchaseOnServer(purchase);

if (isValid) {
await deliverContent(purchase.productId);
}

// Finish transaction
await iap.finishTransaction(
purchase: purchase,
isConsumable: false, // Adjust based on product type
);
}
}

Best Practices

What are the key best practices?

  1. Always set up listeners first before making purchase requests
  2. Validate purchases server-side for security
  3. Use correct isConsumable flag - it handles consume/acknowledge automatically
  4. Handle errors gracefully with proper error codes
  5. Test thoroughly in sandbox/test environments
  6. Initialize connection early in app lifecycle
  7. Cancel subscriptions in dispose to prevent memory leaks
class BestPracticeExample extends StatefulWidget {

State<BestPracticeExample> createState() => _BestPracticeExampleState();
}

class _BestPracticeExampleState extends State<BestPracticeExample> {
final _iap = FlutterInappPurchase.instance;
StreamSubscription? _purchaseUpdatedSubscription;
StreamSubscription? _purchaseErrorSubscription;


void initState() {
super.initState();
_initializeIAP();
}

Future<void> _initializeIAP() async {
// 1. Initialize connection early
await _iap.initConnection();

// 2. Set up listeners before any purchase requests
_purchaseUpdatedSubscription = _iap.purchaseUpdatedListener.listen(
(purchase) async {
// 3. Always verify server-side
final isValid = await verifyPurchaseOnServer(purchase);
if (!isValid) return;

await deliverContent(purchase.productId);

// 4. Use correct isConsumable flag
await _iap.finishTransaction(
purchase: purchase,
isConsumable: true,
);
},
);

_purchaseErrorSubscription = _iap.purchaseErrorListener.listen(
(error) {
// 5. Handle errors gracefully
_handleError(error);
},
);
}


void dispose() {
// 7. Cancel subscriptions
_purchaseUpdatedSubscription?.cancel();
_purchaseErrorSubscription?.cancel();
super.dispose();
}
}

Additional Resources

Where can I find more help?