Skip to main content

Basic Store Implementation

A simple store implementation demonstrating core flutter_inapp_purchase concepts and basic purchase flow. Perfect for getting started with in-app purchases.

Key Features Demonstrated

  • Connection Management - Initialize and manage store connection
  • Product Loading - Fetch products from both App Store and Google Play
  • Purchase Flow - Complete purchase process with user feedback
  • Transaction Finishing - Properly complete transactions
  • Error Handling - Handle common purchase errors gracefully
  • Platform Differences - Handle iOS and Android specific requirements

Platform Differences

⚠️ Important: This example handles key differences between iOS and Android:

  • iOS: Uses single SKU per request, requires StoreKit configuration
  • Android: Uses SKU arrays, requires Google Play Console setup
  • Receipt Handling: Different receipt formats and validation approaches
  • Transaction States: Platform-specific state management

Complete Implementation

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

void main() {
runApp(BasicStoreApp());
}

class BasicStoreApp extends StatelessWidget {

Widget build(BuildContext context) {
return MaterialApp(
title: 'Basic Store Example',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: BasicStoreScreen(),
);
}
}

class BasicStoreScreen extends StatefulWidget {

_BasicStoreScreenState createState() => _BasicStoreScreenState();
}

class _BasicStoreScreenState extends State<BasicStoreScreen> {
// IAP instance
final FlutterInappPurchase _iap = FlutterInappPurchase.instance;

// State management
bool _isConnected = false;
bool _isLoading = false;
List<IAPItem> _products = [];
String? _errorMessage;
PurchasedItem? _latestPurchase;

// Stream subscriptions
StreamSubscription<PurchasedItem?>? _purchaseSubscription;
StreamSubscription<PurchaseResult?>? _errorSubscription;
StreamSubscription<ConnectionResult>? _connectionSubscription;

// Product IDs - Replace with your actual product IDs
final List<String> _productIds = [
'coins_100',
'coins_500',
'remove_ads',
'premium_upgrade',
];


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


void dispose() {
_purchaseSubscription?.cancel();
_errorSubscription?.cancel();
_connectionSubscription?.cancel();
_iap.endConnection();
super.dispose();
}

/// Initialize the store connection and set up listeners
Future<void> _initializeStore() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});

try {
// Initialize connection
await _iap.initConnection();

// Set up purchase success listener
_purchaseSubscription = FlutterInappPurchase.purchaseUpdated.listen(
(purchase) {
if (purchase != null) {
_handlePurchaseSuccess(purchase);
}
},
onError: (error) {
_showError('Purchase stream error: $error');
},
);

// Set up purchase error listener
_errorSubscription = FlutterInappPurchase.purchaseError.listen(
(error) {
if (error != null) {
_handlePurchaseError(error);
}
},
);

// Set up connection listener
_connectionSubscription = FlutterInappPurchase.connectionUpdated.listen(
(connectionResult) {
setState(() {
_isConnected = connectionResult.connected;
});

if (connectionResult.connected) {
_loadProducts();
}
},
);

setState(() {
_isConnected = true;
});

// Load products
await _loadProducts();

} catch (e) {
_showError('Failed to initialize store: $e');
} finally {
setState(() {
_isLoading = false;
});
}
}

/// Load products from the store
Future<void> _loadProducts() async {
if (!_isConnected) return;

setState(() {
_isLoading = true;
_errorMessage = null;
});

try {
final products = await _iap.getProducts(_productIds);

setState(() {
_products = products;
});

print('✅ Loaded ${products.length} products');
for (final product in products) {
print('Product: ${product.productId} - ${product.localizedPrice}');
}

} catch (e) {
_showError('Failed to load products: $e');
} finally {
setState(() {
_isLoading = false;
});
}
}

/// Handle successful purchase
Future<void> _handlePurchaseSuccess(PurchasedItem purchase) async {
print('✅ Purchase successful: ${purchase.productId}');

setState(() {
_latestPurchase = purchase;
_errorMessage = null;
});

// Show success message
_showSuccessSnackBar('Purchase successful: ${purchase.productId}');

try {
// 1. Here you would typically verify the purchase with your server
final isValid = await _verifyPurchase(purchase);

if (isValid) {
// 2. Deliver the product to the user
await _deliverProduct(purchase.productId);

// 3. Finish the transaction
await _finishTransaction(purchase);

print('✅ Purchase completed and delivered');
} else {
_showError('Purchase verification failed');
}

} catch (e) {
_showError('Error processing purchase: $e');
}
}

/// Handle purchase errors
void _handlePurchaseError(PurchaseResult error) {
print('❌ Purchase failed: ${error.message}');

setState(() {
_latestPurchase = null;
});

// Handle specific error codes
switch (error.responseCode) {
case 1: // User cancelled
// Don't show error for user cancellation
print('User cancelled purchase');
break;

case 2: // Network error
_showError('Network error. Please check your connection and try again.');
break;

case 7: // Already owned
_showError('You already own this item. Try restoring your purchases.');
break;

default:
_showError(error.message ?? 'Purchase failed. Please try again.');
}
}

/// Verify purchase with server (mock implementation)
Future<bool> _verifyPurchase(PurchasedItem purchase) async {
// In a real app, send the receipt to your server for verification
// For this example, we'll just simulate a successful verification
await Future.delayed(Duration(milliseconds: 500));

print('🔍 Verifying purchase: ${purchase.productId}');
print('Receipt: ${purchase.transactionReceipt?.substring(0, 50)}...');

return true; // Assume verification successful
}

/// Deliver the purchased product to the user
Future<void> _deliverProduct(String? productId) async {
if (productId == null) return;

print('🎁 Delivering product: $productId');

// Implement your product delivery logic here
switch (productId) {
case 'coins_100':
// Add 100 coins to user's account
print('Added 100 coins to user account');
break;

case 'coins_500':
// Add 500 coins to user's account
print('Added 500 coins to user account');
break;

case 'remove_ads':
// Remove ads for user
print('Removed ads for user');
break;

case 'premium_upgrade':
// Upgrade user to premium
print('Upgraded user to premium');
break;

default:
print('Unknown product: $productId');
}
}

/// Finish the transaction
Future<void> _finishTransaction(PurchasedItem purchase) async {
try {
if (Platform.isAndroid) {
// For Android, consume the purchase if it's a consumable product
if (purchase.purchaseToken != null) {
await _iap.consumePurchaseAndroid(
purchaseToken: purchase.purchaseToken!,
);
print('✅ Android purchase consumed');
}
} else if (Platform.isIOS) {
// For iOS, finish the transaction
await _iap.finishTransactionIOS(
purchase,
isConsumable: _isConsumableProduct(purchase.productId),
);
print('✅ iOS transaction finished');
}

setState(() {
_latestPurchase = null;
});

} catch (e) {
_showError('Failed to finish transaction: $e');
}
}

/// Check if a product is consumable
bool _isConsumableProduct(String? productId) {
// Define which products are consumable
const consumableProducts = ['coins_100', 'coins_500'];
return consumableProducts.contains(productId);
}

/// Make a purchase
Future<void> _makePurchase(String productId) async {
if (!_isConnected) {
_showError('Not connected to store');
return;
}

setState(() {
_isLoading = true;
_errorMessage = null;
});

try {
final request = RequestPurchase(
ios: RequestPurchaseIOS(
sku: productId,
quantity: 1,
),
android: RequestPurchaseAndroid(
skus: [productId],
),
);

await _iap.requestPurchase(
request: request,
type: PurchaseType.inapp,
);

print('🛒 Purchase requested for: $productId');

} catch (e) {
_showError('Failed to request purchase: $e');
} finally {
setState(() {
_isLoading = false;
});
}
}

/// Restore purchases
Future<void> _restorePurchases() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});

try {
await _iap.restorePurchases();

// Get available purchases
final availablePurchases = await _iap.getAvailableItemsIOS();

if (availablePurchases != null && availablePurchases.isNotEmpty) {
_showSuccessSnackBar('Restored ${availablePurchases.length} purchases');

// Process restored purchases
for (final purchase in availablePurchases) {
await _deliverProduct(purchase.productId);
}
} else {
_showSuccessSnackBar('No purchases to restore');
}

} catch (e) {
_showError('Failed to restore purchases: $e');
} finally {
setState(() {
_isLoading = false;
});
}
}

/// Show error message
void _showError(String message) {
setState(() {
_errorMessage = message;
});

ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
duration: Duration(seconds: 4),
),
);
}

/// Show success message
void _showSuccessSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.green,
duration: Duration(seconds: 3),
),
);
}


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Basic Store'),
backgroundColor: _isConnected ? Colors.green : Colors.red,
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: _loadProducts,
),
IconButton(
icon: Icon(Icons.restore),
onPressed: _restorePurchases,
),
],
),
body: _buildBody(),
);
}

Widget _buildBody() {
return Column(
children: [
// Connection status
_buildConnectionStatus(),

// Error message
if (_errorMessage != null) _buildErrorBanner(),

// Latest purchase info
if (_latestPurchase != null) _buildPurchaseInfo(),

// Products list
Expanded(child: _buildProductsList()),
],
);
}

Widget _buildConnectionStatus() {
return Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
color: _isConnected ? Colors.green[100] : Colors.red[100],
child: Row(
children: [
Icon(
_isConnected ? Icons.cloud_done : Icons.cloud_off,
color: _isConnected ? Colors.green[800] : Colors.red[800],
),
SizedBox(width: 8),
Text(
_isConnected ? 'Connected to Store' : 'Not Connected',
style: TextStyle(
color: _isConnected ? Colors.green[800] : Colors.red[800],
fontWeight: FontWeight.w600,
),
),
Spacer(),
if (_isLoading) SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
],
),
);
}

Widget _buildErrorBanner() {
return Container(
width: double.infinity,
padding: EdgeInsets.all(16),
color: Colors.red[50],
child: Row(
children: [
Icon(Icons.error, color: Colors.red[800]),
SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: TextStyle(color: Colors.red[800]),
),
),
IconButton(
onPressed: () => setState(() => _errorMessage = null),
icon: Icon(Icons.close, color: Colors.red[800]),
),
],
),
);
}

Widget _buildPurchaseInfo() {
return Container(
width: double.infinity,
padding: EdgeInsets.all(16),
color: Colors.blue[50],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.shopping_cart, color: Colors.blue[800]),
SizedBox(width: 8),
Text(
'Purchase Successful!',
style: TextStyle(
color: Colors.blue[800],
fontWeight: FontWeight.bold,
),
),
],
),
SizedBox(height: 4),
Text(
'Product: ${_latestPurchase!.productId}',
style: TextStyle(color: Colors.blue[700]),
),
Text(
'Transaction: ${_latestPurchase!.transactionId ?? 'N/A'}',
style: TextStyle(color: Colors.blue[700]),
),
],
),
);
}

Widget _buildProductsList() {
if (_isLoading && _products.isEmpty) {
return Center(child: CircularProgressIndicator());
}

if (_products.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.store, size: 64, color: Colors.grey[400]),
SizedBox(height: 16),
Text(
'No products available',
style: TextStyle(fontSize: 18, color: Colors.grey[600]),
),
SizedBox(height: 8),
ElevatedButton(
onPressed: _loadProducts,
child: Text('Reload Products'),
),
],
),
);
}

return ListView.builder(
padding: EdgeInsets.all(16),
itemCount: _products.length,
itemBuilder: (context, index) {
final product = _products[index];
return _buildProductCard(product);
},
);
}

Widget _buildProductCard(IAPItem product) {
return Card(
margin: EdgeInsets.only(bottom: 12),
elevation: 4,
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue[100],
borderRadius: BorderRadius.circular(8),
),
child: Icon(
_getProductIcon(product.productId),
color: Colors.blue[800],
),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.title ?? product.productId ?? 'Unknown',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
if (product.description != null)
Text(
product.description!,
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
],
),
),
],
),
SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
product.localizedPrice ?? product.price ?? 'Unknown',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.green[700],
),
),
ElevatedButton(
onPressed: _isLoading
? null
: () => _makePurchase(product.productId!),
child: Text('Buy Now'),
),
],
),
],
),
),
);
}

IconData _getProductIcon(String? productId) {
switch (productId) {
case 'coins_100':
case 'coins_500':
return Icons.monetization_on;
case 'remove_ads':
return Icons.block;
case 'premium_upgrade':
return Icons.star;
default:
return Icons.shopping_bag;
}
}
}

Key Features Explained

1. Connection Management

await _iap.initConnection();
  • Initializes connection to App Store or Google Play
  • Must be called before any other IAP operations
  • Connection state is monitored via connectionUpdated stream

2. Product Loading

final products = await _iap.getProducts(_productIds);
  • Fetches product information from the store
  • Returns localized pricing and descriptions
  • Product IDs must be configured in store console

3. Purchase Flow

final request = RequestPurchase(
ios: RequestPurchaseIOS(sku: productId, quantity: 1),
android: RequestPurchaseAndroid(skus: [productId]),
);
await _iap.requestPurchase(request: request, type: PurchaseType.inapp);
  • Platform-specific request objects handle iOS/Android differences
  • Purchase result comes through purchaseUpdated stream
  • Errors are delivered via purchaseError stream

4. Transaction Finishing

// iOS
await _iap.finishTransactionIOS(purchase, isConsumable: true);

// Android
await _iap.consumePurchaseAndroid(purchaseToken: token);
  • Essential for completing the purchase flow
  • iOS: finishTransactionIOS for all purchases
  • Android: consumePurchaseAndroid for consumables

5. Error Handling

The example demonstrates handling common error scenarios:

  • User cancellation (don't show error)
  • Network errors (suggest retry)
  • Already owned items (suggest restore)
  • Generic errors (show user-friendly message)

Usage Instructions

  1. Replace Product IDs: Update _productIds with your actual product IDs
  2. Configure Stores:
    • iOS: Add products to App Store Connect
    • Android: Add products to Google Play Console
  3. Implement Server Verification: Replace _verifyPurchase with real server validation
  4. Customize Product Delivery: Update _deliverProduct with your business logic
  5. Style the UI: Customize the UI to match your app's design

Customization Options

Product Types

// For different product types
enum ProductType { consumable, nonConsumable, subscription }

bool _isConsumableProduct(String productId) {
// Your logic to determine consumable products
return ['coins_100', 'coins_500'].contains(productId);
}

Custom Error Handling

void _handlePurchaseError(PurchaseResult error) {
switch (error.responseCode) {
case 1: /* User cancelled */
case 2: /* Network error */
case 7: /* Already owned */
// Add your custom error handling
}
}

Loading States

// Add loading indicators for better UX
bool _isLoading = false;
String? _loadingMessage;

void _setLoading(bool loading, [String? message]) {
setState(() {
_isLoading = loading;
_loadingMessage = message;
});
}

Next Steps