Purchases
Complete guide to implementing in-app purchases with flutter_inapp_purchase v6.0.0, covering everything from basic setup to advanced purchase handling.
Purchase Flow Overview
The in-app purchase flow follows this standardized pattern:
- Initialize Connection - Establish connection with the store
- Setup Purchase Listeners - Listen for purchase updates and errors
- Load Products - Fetch product information from the store
- Request Purchase - Initiate purchase flow
- Handle Updates - Process purchase results via streams
- Deliver Content - Provide purchased content to user
- Finish Transaction - Complete the transaction with the store
Key Concepts
Purchase Types
- Consumable: Can be purchased multiple times (coins, gems, lives)
- Non-Consumable: Purchased once, owned forever (premium features, ad removal)
- Subscriptions: Recurring purchases with auto-renewal
Platform Differences
- iOS: Uses StoreKit 2 (iOS 15.0+) with fallback to StoreKit 1
- Android: Uses Google Play Billing Client v8
- Both platforms use the same API surface in flutter_inapp_purchase
Basic Purchase Flow
1. Setup Purchase Listeners
Before making any purchases, set up listeners to handle purchase updates and errors:
import 'dart:async';
import 'dart:io';
import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
class PurchaseHandler {
final _iap = FlutterInappPurchase.instance;
StreamSubscription<PurchasedItem?>? _purchaseUpdatedSubscription;
StreamSubscription<PurchaseResult?>? _purchaseErrorSubscription;
void setupPurchaseListeners() {
// Listen to successful purchases
_purchaseUpdatedSubscription = FlutterInappPurchase.purchaseUpdated.listen(
(purchasedItem) {
if (purchasedItem != null) {
debugPrint('Purchase update received: ${purchasedItem.productId}');
_handlePurchaseUpdate(purchasedItem);
}
},
);
// Listen to purchase errors
_purchaseErrorSubscription = FlutterInappPurchase.purchaseError.listen(
(purchaseError) {
if (purchaseError != null) {
debugPrint('Purchase failed: ${purchaseError.message}');
_handlePurchaseError(purchaseError);
}
},
);
}
void dispose() {
_purchaseUpdatedSubscription?.cancel();
_purchaseErrorSubscription?.cancel();
}
}
2. Using with Hooks (Recommended)
For a more structured approach, use this purchase handler pattern:
class ProductsScreen extends StatefulWidget {
State<ProductsScreen> createState() => _ProductsScreenState();
}
class _ProductsScreenState extends State<ProductsScreen> {
final List<String> productIds = [
'dev.hyo.martie.10bulbs',
'dev.hyo.martie.30bulbs',
];
String? _purchaseResult;
bool _isProcessing = false;
StreamSubscription<PurchasedItem?>? _purchaseUpdatedSubscription;
StreamSubscription<PurchaseResult?>? _purchaseErrorSubscription;
void initState() {
super.initState();
_setupPurchaseListeners();
// Load products after initialization
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) {
_loadProducts();
}
});
}
void dispose() {
_purchaseUpdatedSubscription?.cancel();
_purchaseErrorSubscription?.cancel();
super.dispose();
}
// Purchase listener setup...
}
3. Request a Purchase
Use the new requestPurchase
API for initiating purchases:
Future<void> _handlePurchase(String productId) async {
try {
setState(() {
_isProcessing = true;
_purchaseResult = 'Processing purchase...';
});
// Use the new requestPurchase API
await FlutterInappPurchase.instance.requestPurchase(
request: RequestPurchase(
ios: RequestPurchaseIOS(
sku: productId,
quantity: 1,
),
android: RequestPurchaseAndroid(
skus: [productId],
),
),
type: PurchaseType.inapp, // or PurchaseType.subs for subscriptions
);
} catch (error) {
setState(() {
_isProcessing = false;
_purchaseResult = '❌ Purchase failed: $error';
});
}
}
New Platform-Specific API (v2.7.0+)
New Product Loading API
Future<void> _loadProducts() async {
try {
// Use requestProducts (new API)
await FlutterInappPurchase.instance.requestProducts(
RequestProductsParams(
skus: productIds,
type: PurchaseType.inapp
),
);
// Get products from provider or state management
final products = await FlutterInappPurchase.instance.getProducts(productIds);
debugPrint('Loaded ${products.length} products');
} catch (e) {
debugPrint('Error loading products: $e');
}
}
Legacy API (Still Supported)
// Legacy method - still works but deprecated
final products = await FlutterInappPurchase.instance.getProducts(productIds);
final subscriptions = await FlutterInappPurchase.instance.getSubscriptions(subscriptionIds);
New Subscription API (v2.7.0+)
Subscription Purchase
Future<void> requestSubscription(String productId) async {
await FlutterInappPurchase.instance.requestPurchase(
request: RequestPurchase(
ios: RequestPurchaseIOS(sku: productId),
android: RequestPurchaseAndroid(skus: [productId]),
),
type: PurchaseType.subs,
);
}
Legacy Subscription API
// Legacy method - still supported
await FlutterInappPurchase.instance.requestSubscription(productId);
Important Notes
Purchase Flow Best Practices
- Always set up listeners first before making any purchase requests
- Handle both success and error cases appropriately
- Show loading states during purchase processing
- Validate purchases server-side for security
- Consume consumable products after delivery
Pending Purchases
Handle cases where purchases might be pending:
Future<void> _handlePurchaseUpdate(PurchasedItem purchasedItem) async {
debugPrint('Purchase successful: ${purchasedItem.productId}');
// Deliver the product to the user
await _deliverProduct(purchasedItem.productId);
// Finish the transaction
try {
if (Platform.isAndroid) {
// For Android consumable products - consume the purchase
if (purchasedItem.purchaseToken != null) {
await FlutterInappPurchase.instance.consumePurchaseAndroid(
purchaseToken: purchasedItem.purchaseToken!,
);
debugPrint('Android purchase consumed successfully');
}
} else if (Platform.isIOS) {
// For iOS - finish the transaction
await FlutterInappPurchase.instance.finishTransactionIOS(
purchasedItem,
isConsumable: true, // Set appropriately for your product type
);
debugPrint('iOS transaction finished');
}
} catch (e) {
debugPrint('Error finishing transaction: $e');
}
}
Getting Product Information
Retrieving Product Prices
class ProductInfo {
static Future<List<IAPItem>> loadProductInformation(List<String> productIds) async {
try {
// Request products from store
await FlutterInappPurchase.instance.requestProducts(
RequestProductsParams(skus: productIds, type: PurchaseType.inapp),
);
// Get product details
final products = await FlutterInappPurchase.instance.getProducts(productIds);
for (final product in products) {
debugPrint('Product: ${product.productId}');
debugPrint('Title: ${product.title}');
debugPrint('Description: ${product.description}');
debugPrint('Price: ${product.localizedPrice}');
debugPrint('Currency: ${product.currency}');
}
return products;
} catch (e) {
debugPrint('Error loading product information: $e');
return [];
}
}
}
Platform Support
class PlatformSupport {
static Future<bool> checkPurchaseSupport() async {
try {
if (Platform.isIOS) {
// Check if device can make payments
final canMakePayments = await FlutterInappPurchase.instance.initialize();
return canMakePayments;
} else if (Platform.isAndroid) {
// Check Play Store connection
final connected = await FlutterInappPurchase.instance.initConnection();
return connected == 'connected';
}
return false;
} catch (e) {
debugPrint('Error checking purchase support: $e');
return false;
}
}
}
Checking Platform Compatibility
void checkPlatformFeatures() {
if (Platform.isIOS) {
// iOS-specific features
debugPrint('iOS platform detected');
// Can use iOS-specific methods like:
// - presentCodeRedemptionSheet()
// - showManageSubscriptions()
// - isEligibleForIntroOfferIOS()
} else if (Platform.isAndroid) {
// Android-specific features
debugPrint('Android platform detected');
// Can use Android-specific methods like:
// - consumePurchaseAndroid()
// - deepLinkToSubscriptionsAndroid()
// - getConnectionStateAndroid()
}
}
Product Types
Consumable Products
Products that can be purchased multiple times:
Future<void> handleConsumableProduct(PurchasedItem purchase) async {
// Deliver the consumable content (coins, lives, etc.)
await deliverConsumableProduct(purchase.productId);
// For Android - consume the purchase so it can be bought again
if (Platform.isAndroid && purchase.purchaseToken != null) {
await FlutterInappPurchase.instance.consumePurchaseAndroid(
purchaseToken: purchase.purchaseToken!,
);
}
// For iOS - finish transaction
if (Platform.isIOS) {
await FlutterInappPurchase.instance.finishTransactionIOS(
purchase,
isConsumable: true,
);
}
}
Non-Consumable Products
Products purchased once and owned permanently:
Future<void> handleNonConsumableProduct(PurchasedItem purchase) async {
// Deliver the permanent content (premium features, ad removal)
await deliverPermanentProduct(purchase.productId);
// For Android - acknowledge the purchase (don't consume)
if (Platform.isAndroid && purchase.purchaseToken != null) {
await FlutterInappPurchase.instance.acknowledgePurchaseAndroid(
purchaseToken: purchase.purchaseToken!,
);
}
// For iOS - finish transaction
if (Platform.isIOS) {
await FlutterInappPurchase.instance.finishTransactionIOS(
purchase,
isConsumable: false,
);
}
}
Subscriptions
Recurring purchases with auto-renewal:
Future<void> handleSubscriptionProduct(PurchasedItem purchase) async {
// Activate subscription for user
await activateSubscription(purchase.productId);
// For Android - acknowledge the subscription
if (Platform.isAndroid && purchase.purchaseToken != null) {
await FlutterInappPurchase.instance.acknowledgePurchaseAndroid(
purchaseToken: purchase.purchaseToken!,
);
}
// For iOS - finish transaction
if (Platform.isIOS) {
await FlutterInappPurchase.instance.finishTransactionIOS(
purchase,
isConsumable: false,
);
}
}
Advanced Purchase Handling
Purchase Restoration
Restore previously purchased items:
Future<void> restorePurchases() async {
try {
// Restore completed transactions
await FlutterInappPurchase.instance.restorePurchases();
// Get available purchases
final purchases = await FlutterInappPurchase.instance.getAvailablePurchases();
debugPrint('Restored ${purchases.length} purchases');
// Process each restored purchase
for (final purchase in purchases) {
await _deliverProduct(purchase.productId);
}
} catch (e) {
debugPrint('Error restoring purchases: $e');
}
}
Handling Pending Purchases
Handle purchases that are pending approval:
void _handlePurchaseError(PurchaseResult error) {
debugPrint('Purchase failed: ${error.message}');
// Check if error is "You already own this item" (Error code 7)
if (error.responseCode == 7 || error.message?.contains('already own') == true) {
debugPrint('User already owns this item. Attempting to consume existing purchase...');
_consumeExistingPurchase();
} else {
// Handle other errors
_showErrorDialog(error.message ?? 'Unknown error occurred');
}
}
Future<void> _consumeExistingPurchase() async {
try {
// Restore purchases to get all owned items
await FlutterInappPurchase.instance.restorePurchases();
// Get available purchases
final purchases = await FlutterInappPurchase.instance.getAvailablePurchases();
// Find and consume purchases for our product IDs
for (final purchase in purchases) {
if (productIds.contains(purchase.productId)) {
if (purchase.purchaseToken != null) {
await FlutterInappPurchase.instance.consumePurchaseAndroid(
purchaseToken: purchase.purchaseToken!,
);
debugPrint('Successfully consumed: ${purchase.productId}');
}
}
}
} catch (e) {
debugPrint('Error during consume process: $e');
}
}
Subscription Management
Open native subscription management:
Future<void> openSubscriptionManagement() async {
try {
if (Platform.isIOS) {
await FlutterInappPurchase.instance.showManageSubscriptions();
} else if (Platform.isAndroid) {
await FlutterInappPurchase.instance.deepLinkToSubscriptionsAndroid();
}
} catch (e) {
debugPrint('Failed to open subscription management: $e');
}
}
Receipt Validation
Validate purchases server-side for security:
Future<bool> validatePurchaseReceipt(PurchasedItem purchase) async {
try {
if (Platform.isIOS) {
// Validate iOS receipt
final result = await FlutterInappPurchase.instance.validateReceiptIos(
receiptBody: {
'receipt-data': purchase.transactionReceipt,
'password': 'your-shared-secret', // From App Store Connect
},
isTest: true, // Set to false for production
);
return result != null && result['status'] == 0;
} else if (Platform.isAndroid) {
// Validate Android purchase
final result = await FlutterInappPurchase.instance.validateReceiptAndroid(
packageName: 'your.package.name',
productId: purchase.productId!,
productToken: purchase.purchaseToken!,
accessToken: 'your-access-token', // From Google Play Console
isSubscription: false,
);
return result != null;
}
return false;
} catch (e) {
debugPrint('Receipt validation failed: $e');
return false;
}
}
Error Handling
Common Purchase Errors
void handlePurchaseError(PurchaseResult error) {
switch (error.responseCode) {
case 1: // User cancelled
debugPrint('User cancelled the purchase');
break;
case 2: // Network error
debugPrint('Network error occurred');
break;
case 3: // Billing unavailable
debugPrint('Billing service unavailable');
break;
case 4: // Item unavailable
debugPrint('Requested item is unavailable');
break;
case 5: // Developer error
debugPrint('Invalid arguments provided to the API');
break;
case 6: // Error
debugPrint('Fatal error during the API action');
break;
case 7: // Item already owned
debugPrint('User already owns this item');
_handleAlreadyOwned();
break;
case 8: // Item not owned
debugPrint('User does not own this item');
break;
default:
debugPrint('Unknown error: ${error.message}');
}
}
Testing Purchases
iOS Testing
Set up iOS testing environment:
// For iOS testing in sandbox environment
void setupIOSTesting() {
debugPrint('Testing on iOS Sandbox');
// Use test Apple ID for sandbox testing
// Products must be configured in App Store Connect
// Test with different sandbox user accounts
}
Android Testing
Set up Android testing environment:
// For Android testing with test purchases
void setupAndroidTesting() {
debugPrint('Testing on Android');
// Use test product IDs like:
// - android.test.purchased
// - android.test.canceled
// - android.test.refunded
// - android.test.item_unavailable
final testProductIds = [
'android.test.purchased', // Always succeeds
'android.test.canceled', // Always cancelled
];
}
Complete Example
Here's a complete working example based on the project's products_screen.dart
:
class PurchaseService {
final _iap = FlutterInappPurchase.instance;
StreamSubscription<PurchasedItem?>? _purchaseUpdatedSubscription;
StreamSubscription<PurchaseResult?>? _purchaseErrorSubscription;
void init() {
_setupPurchaseListeners();
}
void _setupPurchaseListeners() {
_purchaseUpdatedSubscription = FlutterInappPurchase.purchaseUpdated.listen(
(purchasedItem) {
if (purchasedItem != null) {
_handlePurchaseSuccess(purchasedItem);
}
},
);
_purchaseErrorSubscription = FlutterInappPurchase.purchaseError.listen(
(purchaseError) {
if (purchaseError != null) {
_handlePurchaseError(purchaseError);
}
},
);
}
Future<void> _handlePurchaseSuccess(PurchasedItem purchase) async {
// 1. Deliver product
await _deliverProduct(purchase.productId);
// 2. Finish transaction
if (Platform.isAndroid && purchase.purchaseToken != null) {
await _iap.consumePurchaseAndroid(
purchaseToken: purchase.purchaseToken!,
);
} else if (Platform.isIOS) {
await _iap.finishTransactionIOS(purchase, isConsumable: true);
}
}
void _handlePurchaseError(PurchaseResult error) {
if (error.responseCode == 7) {
// Handle "already owned" error
_consumeExistingPurchases();
}
}
Future<void> purchaseProduct(String productId) async {
await _iap.requestPurchase(
request: RequestPurchase(
ios: RequestPurchaseIOS(sku: productId, quantity: 1),
android: RequestPurchaseAndroid(skus: [productId]),
),
type: PurchaseType.inapp,
);
}
void dispose() {
_purchaseUpdatedSubscription?.cancel();
_purchaseErrorSubscription?.cancel();
}
}
This guide covers the complete purchase flow using the actual flutter_inapp_purchase v6.0.0 API, with examples based on the working code from your project.