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:

YAML
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:

Dart
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:

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(),
    ),
  );
}

// ... (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 the provider package, making the ProviderModel available to the entire app.
  • initState: Calls initApp to begin the IAP setup process when the widget is initialized.
  • initApp: Gets an instance of InAppPurchase and calls the appropriate functions.
  • Provider.of: Used to access the ProviderModel and its unlockProVersion property to conditionally show ads.

4. Purchase Logic (payment_func.dart)

payment_func.dart contains the core logic for handling purchases and verifying them:

Dart
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 the unlockProVersion state in the ProviderModel. 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:

Dart
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

Popular posts from this blog

Flutter Local Scheduled Notifications

Easy migration of old Flutter project to the latest version by creating a brand new flutter project | java to kotlin - null safety - material3 - embedding android v2 - all migrations at once