Flutter In-App Purchases one time payment (non consumable) Remove ads Unlock pro premium version: A Code Walkthrough (2025)
This blog post breaks down a simple in-app purchase (IAP) implementation in Flutter, explaining the key files and their roles. We'll explore how to set up IAP, manage purchase status, and handle purchase verification.
1. Project Setup
-The pubspec.yaml
file defines the project's dependencies. For IAP, we need the in_app_purchase
and provider
packages:
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
in_app_purchase: ^3.2.1 # Or latest
provider: ^6.1.2 # For state management
-Dont forget to add internet permission to your AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET"/>
2. State Management (provider.dart
)
provider.dart
uses the provider
package to manage the app's state, specifically whether the user has unlocked the premium version:
import 'package:flutter/foundation.dart'; // Import foundation
class ProviderModel with ChangeNotifier {
bool _unlockProVersion = false;
bool get unlockProVersion => _unlockProVersion;
set unlockProVersion(bool value) {
_unlockProVersion = value;
notifyListeners(); // Notify listeners of state changes
}
}
This file creates a ProviderModel
class that holds the unlockProVersion
boolean. The notifyListeners()
call is crucial; it alerts widgets that depend on this data to rebuild when the value changes.
3. Main Application (main.dart
)
main.dart
sets up the app's structure and initializes the IAP functionality:
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:inapp_purchases_test/payment_func.dart';
import 'package:inapp_purchases_test/payment_screen.dart';
import 'package:inapp_purchases_test/provider.dart';
import 'package:provider/provider.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<ProviderModel>(
create: (_) => ProviderModel(),
),
],
child: const MyApp(),
),
);
}
// ... (MyApp and MyHomePage classes)
class _MyHomePageState extends State<MyHomePage> {
@override
void initState() {
super.initState();
final provider = Provider.of<ProviderModel>(context, listen: false); // Get provider instance
initApp(provider); // Initialize IAP
}
initApp(provider) async {
final InAppPurchase inAppPurchase = InAppPurchase.instance;
// ... logic to handle purchases and verify them
await PaymentFunc().verifyPreviousPurchases(provider, /* purchases, */ inAppPurchase);
}
@override
Widget build(BuildContext context) {
var provider = Provider.of<ProviderModel>(context); // Access the provider
return Scaffold(
// ... (App bar and body)
body: Center(
child: Column(
children: [
// ... other widgets
ElevatedButton(
onPressed: () {
Navigator.push(
context,
CupertinoPageRoute(
fullscreenDialog: true,
builder: (context) => PaymentScreen(),
),
);
},
child: const Text("Payment Screen"),
),
if (!provider.unlockProVersion) // Conditionally show ads
Container(
// ... (Ad container)
),
],
),
),
);
}
}
Key points in main.dart
:
MultiProvider
: Sets up theprovider
package, making theProviderModel
available to the entire app.initState
: CallsinitApp
to begin the IAP setup process when the widget is initialized.initApp
: Gets an instance ofInAppPurchase
and calls the appropriate functions.Provider.of
: Used to access theProviderModel
and itsunlockProVersion
property to conditionally show ads.
4. Purchase Logic (payment_func.dart
)
payment_func.dart
contains the core logic for handling purchases and verifying them:
import 'package:in_app_purchase_storekit/store_kit_wrappers.dart';
class PaymentFunc {
verifyPreviousPurchases(provider, purchases, inAppPurchase) async {
await inAppPurchase.restorePurchases(); // Restore previous purchases
await Future.delayed(const Duration(milliseconds: 100), () async {
for (var pur in purchases) {
if (pur.productID.contains('in_app_payment_test')) {
provider.unlockProVersion = true;
}
}
});
}
}
class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper {
// ... (iOS specific payment queue delegate)
}
verifyPreviousPurchases
: This function is responsible for restoring previous purchases and updating theunlockProVersion
state in theProviderModel
. It's vital to verify purchases, and in a real app, this should be done on a secure server.
5. Payment Screen (payment_screen.dart
)
payment_screen.dart
is where the user interacts with the available in-app purchases. It's responsible for listing products, initiating purchases, and handling purchase updates. A complete implementation requires platform-specific code and interaction with the app store APIs, so it's often the most complex part. A simplified structure would look like this:
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:provider/provider.dart';
class PaymentScreen extends StatefulWidget {
// ...
}
class _PaymentScreenState extends State<PaymentScreen> {
final InAppPurchase _inAppPurchase = InAppPurchase.instance;
List<ProductDetails> _products = [];
StreamSubscription<List<PurchaseDetails>>? _subscription;
@override
void initState() {
super.initState();
_initialize(); // Fetch products and set up listeners
}
Future<void> _initialize() async {
// 1. Fetch available products
// 2. Set up purchase stream listener
}
void _listenToPurchaseUpdated(List<PurchaseDetails> purchases) {
// Handle purchase updates (success, failure, pending)
}
Future<bool> _verifyPurchase(PurchaseDetails purchase) async {
// Server-side verification is essential!
return true; // Placeholder
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
// ... (Display available products and "Buy" buttons)
);
}
}
This file is where you'd use the in_app_purchase
package to fetch product details, display them to the user, and initiate the purchase process. Critically, you must implement server-side purchase verification in the _verifyPurchase
function for a production app.
This walkthrough provides a high-level overview of the code. Implementing IAP fully requires careful handling of platform-specific code, product setup in the app stores, and robust error management. Refer to the official in_app_purchase
package documentation for complete and up-to-date information.
All files:
main.dart file:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:inapp_purchases_test/payment_func.dart';
import 'package:inapp_purchases_test/payment_screen.dart';
import 'package:inapp_purchases_test/provider.dart';
import 'package:provider/provider.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MultiProvider(providers: [
ChangeNotifierProvider<ProviderModel>(
create: (_) => ProviderModel(),
),
], child: const MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'InApp Purchases'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
void initState() {
final provider = Provider.of<ProviderModel>(context, listen: false);
initApp(provider);
super.initState();
}
@override
void dispose() {
PaymentScreenState().dispose();
super.dispose();
}
initApp(provider) async {
// await Future.delayed(Duration(milliseconds: 500));
final InAppPurchase inAppPurchase = InAppPurchase.instance;
await PaymentScreenState().inAppStream(provider);
await PaymentFunc()
.verifyPreviousPurchases(provider, purchases, inAppPurchase);
}
@override
Widget build(BuildContext context) {
var provider = Provider.of<ProviderModel>(context);
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Spacer(),
ElevatedButton(
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(Colors.green)),
onPressed: () {
Navigator.of(context).push(
CupertinoPageRoute(
fullscreenDialog: true,
builder: (context) {
return PaymentScreen();
},
),
);
},
child: Text(
"Payment Screen",
style: TextStyle(color: Colors.white),
)),
Spacer(),
if (!provider.unlockProVersion)
Container(
height: 50,
width: double.infinity,
decoration: BoxDecoration(
color: Colors.amberAccent,
),
child: Center(
child: Text(
"Ads",
style: TextStyle(color: Colors.black),
)),
)
],
),
),
);
}
}
payment_screen.dart
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_storekit/in_app_purchase_storekit.dart';
import 'package:flutter/foundation.dart';
import 'package:inapp_purchases_test/payment_func.dart';
import 'package:inapp_purchases_test/provider.dart';
import 'package:provider/provider.dart';
List<PurchaseDetails> purchases = [];
List<ProductDetails> products = [];
const String kUpgradeId = 'in_app_payment_test';
const List<String> _kProductIds = <String>[
kUpgradeId,
];
class PaymentScreen extends StatefulWidget {
const PaymentScreen({super.key});
@override
State<PaymentScreen> createState() => PaymentScreenState();
}
class PaymentScreenState extends State<PaymentScreen> {
final InAppPurchase inAppPurchase = InAppPurchase.instance;
late StreamSubscription<List<PurchaseDetails>> subscription;
List<String> notFoundIds = [];
List<PurchaseDetails> _purchases = <PurchaseDetails>[];
bool isAvailable = false;
bool purchasePending = false;
bool loading = true;
String? queryProductError;
@override
void initState() {
final provider = Provider.of<ProviderModel>(context, listen: false);
initInApp(provider);
super.initState();
}
@override
void dispose() {
subscription.cancel();
super.dispose();
}
Future<void> initInApp(provider) async {
await inAppStream(provider);
await initStoreInfo();
}
Future<void> inAppStream(provider) async {
final Stream<List<PurchaseDetails>> purchaseUpdated =
inAppPurchase.purchaseStream;
subscription = purchaseUpdated.listen((purchaseDetailsList) {
_listenToPurchaseUpdated(purchaseDetailsList, provider);
}, onDone: () {
subscription.cancel();
}, onError: (error) {
// handle error here.
});
}
Future<void> initStoreInfo() async {
final bool isAvailableStore = await inAppPurchase.isAvailable();
if (!isAvailableStore) {
setState(() {
isAvailable = isAvailableStore;
products = [];
purchases = [];
notFoundIds = [];
purchasePending = false;
loading = false;
});
return;
}
if (Platform.isIOS) {
var iosPlatformAddition = inAppPurchase
.getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate());
}
ProductDetailsResponse productDetailResponse =
await inAppPurchase.queryProductDetails(_kProductIds.toSet());
if (productDetailResponse.error != null) {
setState(() {
queryProductError = productDetailResponse.error!.message;
isAvailable = isAvailableStore;
products = productDetailResponse.productDetails;
purchases = [];
notFoundIds = productDetailResponse.notFoundIDs;
purchasePending = false;
loading = false;
});
return;
}
if (productDetailResponse.productDetails.isEmpty) {
setState(() {
queryProductError = null;
isAvailable = isAvailableStore;
products = productDetailResponse.productDetails;
purchases = [];
notFoundIds = productDetailResponse.notFoundIDs;
purchasePending = false;
loading = false;
});
return;
}
setState(() {
isAvailable = isAvailableStore;
products = productDetailResponse.productDetails;
notFoundIds = productDetailResponse.notFoundIDs;
purchasePending = false;
loading = false;
});
}
void showPendingUI() {
purchasePending = true;
}
void deliverProduct(PurchaseDetails purchaseDetails) async {
// IMPORTANT!! Always verify purchase details before delivering the product.
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.
}
Future<void> _listenToPurchaseUpdated(
List<PurchaseDetails> purchaseDetailsList, provider) async {
for (PurchaseDetails purchaseDetails in purchaseDetailsList) {
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 (purchaseDetails.pendingCompletePurchase) {
await inAppPurchase.completePurchase(purchaseDetails);
if (purchaseDetails.productID == 'consumable_product') {
if (kDebugMode) {
print('================================You got coins');
}
}
await PaymentFunc()
.verifyPreviousPurchases(provider, purchases, inAppPurchase);
}
}
}
}
Future<void> confirmPriceChange(BuildContext context) async {
if (Platform.isIOS) {
var iapIosPlatformAddition = inAppPurchase
.getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
await iapIosPlatformAddition.showPriceConsentIfNeeded();
}
}
@override
Widget build(BuildContext context) {
var provider = Provider.of<ProviderModel>(context);
final List<Widget> stack = <Widget>[];
if (queryProductError == null) {
stack.add(
ListView(
children: <Widget>[
_buildConnectionCheckTile(),
_buildProductList(),
_buildRestoreButton(provider),
],
),
);
} else {
stack.add(Center(
child: Text(queryProductError!),
));
}
if (purchasePending) {
stack.add(
const Stack(
children: <Widget>[
Opacity(
opacity: 0.3,
child: ModalBarrier(dismissible: false, color: Colors.grey),
),
Center(
child: CircularProgressIndicator(),
),
],
),
);
}
return Scaffold(
appBar: AppBar(
title: const Text('IAP Example'),
),
body: Stack(
children: stack,
),
);
}
Card _buildConnectionCheckTile() {
if (loading) {
return const Card(child: ListTile(title: Text('Trying to connect...')));
}
final Widget storeHeader = ListTile(
leading: Icon(isAvailable ? Icons.check : Icons.block,
color:
isAvailable ? Colors.green : ThemeData.light().colorScheme.error),
title: Text('The store is ${isAvailable ? 'available' : 'unavailable'}.'),
);
final List<Widget> children = <Widget>[storeHeader];
if (!isAvailable) {
children.addAll(<Widget>[
const Divider(),
ListTile(
title: Text('Not connected',
style: TextStyle(color: ThemeData.light().colorScheme.error)),
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() {
if (loading) {
return const Card(
child: ListTile(
leading: CircularProgressIndicator(),
title: Text('Fetching products...')));
}
if (!isAvailable) {
return const Card();
}
const ListTile productHeader = ListTile(title: Text('Products for Sale'));
final List<ListTile> productList = <ListTile>[];
if (notFoundIds.isNotEmpty) {
productList.add(ListTile(
title: Text('[${notFoundIds.join(", ")}] not found',
style: TextStyle(color: ThemeData.light().colorScheme.error)),
subtitle: const Text(
'This app needs special configuration to run. Please see example/README.md for instructions.')));
}
// 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.
final Map<String, PurchaseDetails> purchases =
Map<String, PurchaseDetails>.fromEntries(
_purchases.map((PurchaseDetails purchase) {
if (purchase.pendingCompletePurchase) {
inAppPurchase.completePurchase(purchase);
}
return MapEntry<String, PurchaseDetails>(purchase.productID, purchase);
}));
productList.addAll(products.map(
(ProductDetails productDetails) {
final PurchaseDetails? previousPurchase = purchases[productDetails.id];
return ListTile(
title: Text(
productDetails.title,
),
subtitle: Text(
productDetails.description,
),
trailing: previousPurchase != null && Platform.isIOS
? IconButton(
onPressed: () => confirmPriceChange(context),
icon: const Icon(Icons.upgrade))
: TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.green[800],
foregroundColor: Colors.white,
),
onPressed: () {
late PurchaseParam purchaseParam;
purchaseParam = PurchaseParam(
productDetails: productDetails,
);
inAppPurchase.buyNonConsumable(
purchaseParam: purchaseParam);
},
child: Text(productDetails.price),
),
);
},
));
return Card(
child: Column(
children: <Widget>[productHeader, const Divider()] + productList));
}
Widget _buildRestoreButton(provider) {
if (loading) {
return Container();
}
return Padding(
padding: const EdgeInsets.all(4.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Text(provider.unlockProVersion
? "Payed for ads"
: "Not payed for Ads"),
TextButton(
style: TextButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
),
onPressed: () => inAppPurchase.restorePurchases(),
child: const Text('Restore purchases'),
),
],
),
);
}
}
payment_func.dart
import 'package:in_app_purchase_storekit/store_kit_wrappers.dart';
class PaymentFunc {
verifyPreviousPurchases(provider, purchases, inAppPurchase) async {
await inAppPurchase.restorePurchases();
await Future.delayed(const Duration(milliseconds: 100), () async {
for (var pur in purchases) {
if (pur.productID.contains('in_app_payment_test')) {
provider.unlockProVersion = true;
}
}
});
}
}
class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper {
@override
bool shouldContinueTransaction(
SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) {
return true;
}
@override
bool shouldShowPriceConsent() {
return false;
}
}
provider.dart
import 'package:flutter/cupertino.dart';
class ProviderModel with ChangeNotifier {
bool _unlockProVersion = false;
bool get unlockProVersion => _unlockProVersion;
set unlockProVersion(bool value) {
_unlockProVersion = value;
notifyListeners();
}
}
pubspec.yaml
name: inapp_purchases_test
description: "A new Flutter project."
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ^3.6.1
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
in_app_purchase: ^3.2.1
provider: ^6.1.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
uses-material-design: true
Comments
Post a Comment