In-App Purchase with null safety in Flutter 2.5.
In this guide, we will explain how to Implement Consumables Non-Consumables and Subscriptions using the in_app_purchase plugin in your flutter App.
1- Add these permissions to
android/app/src/main/AndroidManifest.xml
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.INTERNET"/>
2- Make sure you are using your own product IDs.
3- Add these to your package’s pubspec.yaml file:
in_app_purchase: ^1.0.9
provider: ^6.0.1
shared_preferences: ^2.0.8
NOTE :
If you want to use in_app_purchase 2.0.0 version or above follow the below instructions.
# Rename in_app_purchase_ios to in_app_purchase_storekit.
# Rename InAppPurchaseIosPlatform to InAppPurchaseStoreKitPlatform.
# Rename InAppPurchaseIosPlatformAddition to InAppPurchaseStoreKitPlatformAddition.
This is our file: main.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
import 'package:inapp_purchases_test/homescreen.dart';
import 'package:provider/provider.dart';
import 'providermodel.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
if (defaultTargetPlatform == TargetPlatform.android) {
// For play billing library 2.0 on Android, it is mandatory to call
// [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases)
// as part of initializing the app.
InAppPurchaseAndroidPlatformAddition.enablePendingPurchases();
}
runApp(ChangeNotifierProvider(
create: (context) => ProviderModel(),
child: MaterialApp(debugShowCheckedModeBanner: false,
home: HomeScreen(),
),
));
}
Here is our first Screen UI file: homescreen.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:inapp_purchases_test/paymentScreen.dart';
import 'package:provider/provider.dart';
import 'providermodel.dart';
import 'package:in_app_purchase_ios/in_app_purchase_ios.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
late ProviderModel _appProvider;
@override
void initState() {
final provider = Provider.of<ProviderModel>(context, listen: false);
_appProvider = provider;
SchedulerBinding.instance!.addPostFrameCallback((_) async {
initInApp(provider);
});
super.initState();
}
initInApp(provider) async {
await provider.initInApp();
}
@override
void dispose() {
if (Platform.isIOS) {
var iosPlatformAddition = _appProvider.inAppPurchase
.getPlatformAddition<InAppPurchaseIosPlatformAddition>();
iosPlatformAddition.setDelegate(null);
}
_appProvider.subscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final provider = Provider.of<ProviderModel>(context);
return Scaffold(
appBar: AppBar(
actions: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all<Color>(Colors.green)),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => PaymentScreen()),
);
},
child: Text('Pay')),
)
],
),
body: ListView(
padding: EdgeInsets.all(8),
children: [
Text(
'Non Consumable:',
style: TextStyle(fontSize: 20),
),
Text(
!provider.finishedLoad
? ''
: provider.removeAds
? 'You paid for removing Ads.'
: 'You have not paid for removing Ads.',
style: TextStyle(
color: provider.removeAds ? Colors.green : Colors.grey,
fontSize: 20),
),
Container(
height: 30,
),
Text(
'Silver Subscription:',
style: TextStyle(fontSize: 20),
),
Text(
!provider.finishedLoad
? ''
: provider.silverSubscription
? 'You have Silver Subscription.'
: 'You have not paid for Silver Subscription.',
style: TextStyle(
color: provider.silverSubscription ? Colors.green : Colors.grey,
fontSize: 20),
),
Container(
height: 30,
),
Text(
'Gold Subscription:',
style: TextStyle(fontSize: 20),
),
Text(
!provider.finishedLoad
? ''
: provider.goldSubscription
? 'You have Gold Subscription.'
: 'You have not paid for Gold Subscription.',
style: TextStyle(
color: provider.goldSubscription ? Colors.green : Colors.grey,
fontSize: 20),
),
Container(
height: 30,
),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Purchased consumables:${provider.consumables.length}',
style: TextStyle(fontSize: 20)),
_buildConsumableBox(provider),
],
)
],
),
);
}
Card _buildConsumableBox(provider) {
if (provider.loading) {
return Card(
child: (ListTile(
leading: CircularProgressIndicator(),
title: Text('Fetching consumables...'))));
}
if (!provider.isAvailable || provider.notFoundIds.contains(kConsumableId)) {
return Card();
}
final List<Widget> tokens = provider.consumables.map<Widget>((String id) {
return GridTile(
child: IconButton(
icon: Icon(
Icons.stars,
size: 42.0,
color: Colors.orange,
),
splashColor: Colors.yellowAccent,
onPressed: () {
provider.consume(id);
},
),
);
}).toList();
return Card(
elevation: 0,
child: Column(children: <Widget>[
GridView.count(
crossAxisCount: 5,
children: tokens,
shrinkWrap: true,
)
]));
}
}
This is our paymentScreen.dart UI screen file which shows our product list where users pay for the product.
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:in_app_purchase_android/billing_client_wrappers.dart';
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
import 'package:in_app_purchase_ios/in_app_purchase_ios.dart';
import 'package:provider/provider.dart';
import 'providermodel.dart';
class PaymentScreen extends StatefulWidget {
@override
_PaymentScreenState createState() => _PaymentScreenState();
}
class _PaymentScreenState extends State<PaymentScreen> {
late ProviderModel _appProvider;
@override
void initState() {
final provider = Provider.of<ProviderModel>(context, listen: false);
_appProvider = provider;
inAppStream(provider);
super.initState();
}
inAppStream(provider) async {
await provider.inAppStream();
}
@override
void dispose() {
if (Platform.isIOS) {
var iosPlatformAddition = _appProvider.inAppPurchase
.getPlatformAddition<InAppPurchaseIosPlatformAddition>();
iosPlatformAddition.setDelegate(null);
}
_appProvider.subscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final provider = Provider.of<ProviderModel>(context);
List<Widget> stack = [];
if (provider.queryProductError == null) {
stack.add(
ListView(
children: [
_buildConnectionCheckTile(provider),
_buildProductList(provider),
],
),
);
} else {
stack.add(Center(
child: Text(provider.queryProductError!),
));
}
if (provider.purchasePending) {
stack.add(
Stack(
children: [
Opacity(
opacity: 0.3,
child: const ModalBarrier(dismissible: false, color: Colors.grey),
),
Center(
child: CircularProgressIndicator(),
),
],
),
);
}
return Scaffold(
appBar: AppBar(
title: const Text('IAP Example'),
),
body: Stack(
children: stack,
),
);
}
Card _buildConnectionCheckTile(provider) {
if (provider.loading) {
return Card(child: ListTile(title: const Text('Trying to connect...')));
}
final Widget storeHeader = provider.notFoundIds.isNotEmpty
? ListTile(
leading: Icon(Icons.block,
color: provider.isAvailable
? Colors.grey
: ThemeData.light().errorColor),
title: Text('The store is unavailable'))
: ListTile(
leading: Icon(Icons.check, color: Colors.green),
title: Text('The store is available'),
);
final List<Widget> children = <Widget>[storeHeader];
if (!provider.isAvailable) {
children.addAll([
Divider(),
ListTile(
title: Text('Not connected',
style: TextStyle(color: ThemeData.light().errorColor)),
subtitle: const Text(
'Unable to connect to the payments processor. Has this app been configured correctly? See the example README for instructions.'),
),
]);
}
return Card(child: Column(children: children));
}
Card _buildProductList(provider) {
if (provider.loading) {
return Card(
child: (ListTile(
leading: CircularProgressIndicator(),
title: Text('Fetching products...'))));
}
if (!provider.isAvailable) {
return Card();
}
final ListTile productHeader = ListTile(title: Text('Products for Sale'));
List<ListTile> productList = <ListTile>[];
if (provider.notFoundIds.isNotEmpty) {
productList.add(ListTile(
title: Text('Products not found',
style: TextStyle(color: ThemeData.light().errorColor)),
));
}
// This loading previous purchases code is just a demo. Please do not use this as it is.
// In your app you should always verify the purchase data using the `verificationData` inside the [PurchaseDetails] object before trusting it.
// We recommend that you use your own server to verify the purchase data.
Map<String, PurchaseDetails> purchasesIn =
Map.fromEntries(purchases.map((PurchaseDetails purchase) {
if (purchase.pendingCompletePurchase) {
provider.inAppPurchase.completePurchase(purchase);
}
return MapEntry<String, PurchaseDetails>(purchase.productID, purchase);
}));
productList.addAll(products.map(
(ProductDetails productDetails) {
PurchaseDetails? previousPurchase = purchasesIn[productDetails.id];
return ListTile(
title: Text(
productDetails.title,
),
subtitle: Text(
productDetails.description,
),
trailing: previousPurchase != null
? IconButton(
onPressed: () => provider.confirmPriceChange(context),
icon: Icon(
Icons.check,
color: Colors.green,
size: 40,
))
: TextButton(
child: Text(productDetails.id == kConsumableId &&
provider.consumables.length > 0
? "Buy more\n${productDetails.price}"
: productDetails.price),
style: TextButton.styleFrom(
backgroundColor: Colors.green[800],
primary: Colors.white,
),
onPressed: () {
late PurchaseParam purchaseParam;
if (Platform.isAndroid) {
// NOTE: If you are making a subscription purchase/upgrade/downgrade, we recommend you to
// verify the latest status of you your subscription by using server side receipt validation
// and update the UI accordingly. The subscription purchase status shown
// inside the app may not be accurate.
final oldSubscription = provider.getOldSubscription(
productDetails, purchasesIn);
purchaseParam = GooglePlayPurchaseParam(
productDetails: productDetails,
applicationUserName: null,
changeSubscriptionParam: (oldSubscription != null)
? ChangeSubscriptionParam(
oldPurchaseDetails: oldSubscription,
prorationMode: ProrationMode
.immediateWithTimeProration,
)
: null);
} else {
purchaseParam = PurchaseParam(
productDetails: productDetails,
applicationUserName: null,
);
}
if (productDetails.id == kConsumableId) {
provider.inAppPurchase.buyConsumable(
purchaseParam: purchaseParam,
autoConsume: kAutoConsume || Platform.isIOS);
} else {
provider.inAppPurchase
.buyNonConsumable(purchaseParam: purchaseParam);
}
},
));
},
));
return Card(
child:
Column(children: <Widget>[productHeader, Divider()] + productList));
}
}
This is our consumable_store.dart file where we save and retrieve consumable products in our device using the shared_preferences package.
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
/// A store of consumable items.
///
/// This is a development prototype tha stores consumables in the shared
/// preferences. Do not use this in real world apps.
class ConsumableStore {
static const String _kPrefKey = 'consumables';
static Future<void> _writes = Future.value();
/// Adds a consumable with ID `id` to the store.
///
/// The consumable is only added after the returned Future is complete.
static Future<void> save(String id) {
_writes = _writes.then((void _) => _doSave(id));
return _writes;
}
/// Consumes a consumable with ID `id` from the store.
///
/// The consumable was only consumed after the returned Future is complete.
static Future<void> consume(String id) {
_writes = _writes.then((void _) => _doConsume(id));
return _writes;
}
/// Returns the list of consumables from the store.
static Future<List<String>> load() async {
return (await SharedPreferences.getInstance()).getStringList(_kPrefKey) ??
[];
}
static Future<void> _doSave(String id) async {
List<String> cached = await load();
SharedPreferences prefs = await SharedPreferences.getInstance();
cached.add(id);
await prefs.setStringList(_kPrefKey, cached);
}
static Future<void> _doConsume(String id) async {
List<String> cached = await load();
SharedPreferences prefs = await SharedPreferences.getInstance();
cached.remove(id);
await prefs.setStringList(_kPrefKey, cached);
}
}
This is our provider model class where we put our logic here.
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:in_app_purchase_android/billing_client_wrappers.dart';
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
import 'package:in_app_purchase_ios/in_app_purchase_ios.dart';
import 'package:in_app_purchase_ios/store_kit_wrappers.dart';
import 'consumable_store.dart';
import 'package:flutter/foundation.dart';
List<PurchaseDetails> purchases = [];
List<ProductDetails> products = [];
const bool kAutoConsume = true;
const String kConsumableId = 'consumable_product';
const String kUpgradeId = 'non_consumable';
const String kSilverSubscriptionId = 'silver_subscription';
const String kGoldSubscriptionId = 'gold_subscription';
const List<String> _kProductIds = <String>[
kConsumableId,
kUpgradeId,
kSilverSubscriptionId,
kGoldSubscriptionId,
];
class ProviderModel with ChangeNotifier {
final InAppPurchase inAppPurchase = InAppPurchase.instance;
late StreamSubscription<List<PurchaseDetails>> subscription;
List<String> notFoundIds = [];
List<String> consumables = [];
bool isAvailable = false;
bool purchasePending = false;
bool loading = true;
String? queryProductError;
Future<void> initInApp() async {
final Stream<List<PurchaseDetails>> purchaseUpdated =
inAppPurchase.purchaseStream;
subscription = purchaseUpdated.listen((purchaseDetailsList) {
_listenToPurchaseUpdated(purchaseDetailsList);
}, onDone: () {
subscription.cancel();
}, onError: (error) {
// handle error here.
});
await initStoreInfo();
await verifyPreviousPurchases();
}
Future<void> inAppStream() async {
final Stream<List<PurchaseDetails>> purchaseUpdated =
inAppPurchase.purchaseStream;
subscription = purchaseUpdated.listen((purchaseDetailsList) {
}, onDone: () {
subscription.cancel();
}, onError: (error) {
// handle error here.
});
}
verifyPreviousPurchases() async {
print("=============================verifyPreviousPurchases");
await inAppPurchase.restorePurchases();
await Future.delayed(const Duration(milliseconds: 100), () {
for (var pur in purchases) {
if (pur.productID.contains('non_consumable')) {
removeAds = true;
}
if (pur.productID.contains('silver_subscription')) {
silverSubscription = true;
}
if (pur.productID.contains('gold_subscription')) {
goldSubscription = true;
}
}
finishedLoad = true;
});
notifyListeners();
}
bool _removeAds = false;
bool get removeAds => _removeAds;
set removeAds(bool value) {
_removeAds = value;
notifyListeners();
}
bool _silverSubscription = false;
bool get silverSubscription => _silverSubscription;
set silverSubscription(bool value) {
_silverSubscription = value;
notifyListeners();
}
bool _goldSubscription = false;
bool get goldSubscription => _goldSubscription;
set goldSubscription(bool value) {
_goldSubscription = value;
notifyListeners();
}
bool _finishedLoad = false;
bool get finishedLoad => _finishedLoad;
set finishedLoad(bool value) {
_finishedLoad = value;
notifyListeners();
}
Future<void> initStoreInfo() async {
final bool isAvailableStore = await inAppPurchase.isAvailable();
if (!isAvailableStore) {
isAvailable = isAvailableStore;
products = [];
purchases = [];
notFoundIds = [];
consumables = [];
purchasePending = false;
loading = false;
return;
}
if (Platform.isIOS) {
var iosPlatformAddition =
inAppPurchase.getPlatformAddition<InAppPurchaseIosPlatformAddition>();
await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate());
}
ProductDetailsResponse productDetailResponse =
await inAppPurchase.queryProductDetails(_kProductIds.toSet());
if (productDetailResponse.error != null) {
queryProductError = productDetailResponse.error!.message;
isAvailable = isAvailableStore;
products = productDetailResponse.productDetails;
purchases = [];
notFoundIds = productDetailResponse.notFoundIDs;
consumables = [];
purchasePending = false;
loading = false;
return;
}
if (productDetailResponse.productDetails.isEmpty) {
queryProductError = null;
isAvailable = isAvailableStore;
products = productDetailResponse.productDetails;
purchases = [];
notFoundIds = productDetailResponse.notFoundIDs;
consumables = [];
purchasePending = false;
loading = false;
return;
}
List<String> consumableProd = await ConsumableStore.load();
isAvailable = isAvailableStore;
products = productDetailResponse.productDetails;
notFoundIds = productDetailResponse.notFoundIDs;
consumables = consumableProd;
purchasePending = false;
loading = false;
notifyListeners();
}
Future<void> consume(String id) async {
await ConsumableStore.consume(id);
final List<String> consumableProd = await ConsumableStore.load();
consumables = consumableProd;
notifyListeners();
}
void showPendingUI() {
purchasePending = true;
}
void deliverProduct(PurchaseDetails purchaseDetails) async {
// IMPORTANT!! Always verify purchase details before delivering the product.
if (purchaseDetails.productID == kConsumableId) {
await ConsumableStore.save(purchaseDetails.purchaseID!);
List<String> consumableProd = await ConsumableStore.load();
purchasePending = false;
consumables = consumableProd;
} else {
purchases.add(purchaseDetails);
purchasePending = false;
}
}
void handleError(IAPError error) {
purchasePending = false;
}
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) {
// IMPORTANT!! Always verify a purchase before delivering the product.
// For the purpose of an example, we directly return true.
return Future<bool>.value(true);
}
void _handleInvalidPurchase(PurchaseDetails purchaseDetails) {
// handle invalid purchase here if _verifyPurchase` failed.
}
void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.pending) {
showPendingUI();
} else {
if (purchaseDetails.status == PurchaseStatus.error) {
handleError(purchaseDetails.error!);
} else if (purchaseDetails.status == PurchaseStatus.purchased ||
purchaseDetails.status == PurchaseStatus.restored) {
bool valid = await _verifyPurchase(purchaseDetails);
if (valid) {
deliverProduct(purchaseDetails);
} else {
_handleInvalidPurchase(purchaseDetails);
return;
}
}
if (Platform.isAndroid) {
if (!kAutoConsume && purchaseDetails.productID == kConsumableId) {
final InAppPurchaseAndroidPlatformAddition androidAddition =
inAppPurchase.getPlatformAddition<
InAppPurchaseAndroidPlatformAddition>();
await androidAddition.consumePurchase(purchaseDetails);
}
}
if (purchaseDetails.pendingCompletePurchase) {
await inAppPurchase.completePurchase(purchaseDetails);
if (purchaseDetails.productID == 'consumable_product') {
print('================================You got coins');
}
verifyPreviousPurchases();
}
}
});
}
Future<void> confirmPriceChange(BuildContext context) async {
if (Platform.isAndroid) {
final InAppPurchaseAndroidPlatformAddition androidAddition = inAppPurchase
.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
var priceChangeConfirmationResult =
await androidAddition.launchPriceChangeConfirmationFlow(
sku: 'purchaseId',
);
if (priceChangeConfirmationResult.responseCode == BillingResponse.ok) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Price change accepted'),
));
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
priceChangeConfirmationResult.debugMessage ??
"Price change failed with code ${priceChangeConfirmationResult.responseCode}",
),
));
}
}
if (Platform.isIOS) {
var iapIosPlatformAddition =
inAppPurchase.getPlatformAddition<InAppPurchaseIosPlatformAddition>();
await iapIosPlatformAddition.showPriceConsentIfNeeded();
}
}
GooglePlayPurchaseDetails? getOldSubscription(
ProductDetails productDetails, Map<String, PurchaseDetails> purchases) {
GooglePlayPurchaseDetails? oldSubscription;
if (productDetails.id == kSilverSubscriptionId &&
purchases[kGoldSubscriptionId] != null) {
oldSubscription =
purchases[kGoldSubscriptionId] as GooglePlayPurchaseDetails;
} else if (productDetails.id == kGoldSubscriptionId &&
purchases[kSilverSubscriptionId] != null) {
oldSubscription =
purchases[kSilverSubscriptionId] as GooglePlayPurchaseDetails;
}
return oldSubscription;
}
// bool removeAds = false;
//
// void removeAdsFunc(newValue) {
// removeAds = newValue;
// notifyListeners();
// }
}
class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper {
@override
bool shouldContinueTransaction(
SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) {
return true;
}
@override
bool shouldShowPriceConsent() {
return false;
}
}