flutterin-app-purchaseflutter-in-app-purchase

Do I really need a backend for in-app purchases?


I am quite new to in-app purchases in flutter. I am writing a side-project app alone, and I would like to allow the user to upgrade the app from a free version to a paid one.

I saw in the documentation that the in_app_purchase package is the good tool for that (after setting up the app stores). I also found this codelab about the topic.

My question is: Is there a way to veriy the purchase Without a backend? I want to do a non-consumable purchache, and I wonder why do I need a backend to verify that? The package returns the items owned by the user, and in my case I belive that is enough.

InAppPurchase.queryPastPurchases()

Is there a flaw in my logic here? In the codelab, the tutorial states that

You can securely verify transactions. | You can react to billing events from the app stores. | You can keep track of the purchases in a database. | Users won't be able to fool your app into providing premium features by rewinding their system clock.

but these seems like unnecessary extra safeguards for me, if I use the said package...

I saw the related questions, but they seem to be more than 10 years old now.


Solution

  • As the other answers tells, the official package does not support verifying purchaches for a reason.

    Hovever, I found that this open source lib https://pub.dev/packages/flutter_inapp_purchase does support the check, and I was able to use it without any problem.

    Here is my implementation for reference.

    import 'dart:async';
    
    import 'package:flutter/material.dart';
    import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
    import 'package:in_app_purchase/in_app_purchase.dart';
    
    import '../constants.dart';
    import '../models/purchasable_product.dart';
    import '../models/store_stat.dart';
    import '../widgets/ad/in_app_connection.dart';
    
    class PaidVersionCheckNotifier extends ChangeNotifier {
      late StreamSubscription<List<PurchaseDetails>> _subscription;
      late StreamSubscription<List<PurchaseDetails>> iapHelperSubscription;
      final iapConnection = IAPConnection.instance;               // official in-app purchache lib
      final iapHelperConnection = FlutterInappPurchase.instance;  // custom, 3d party lib for verifying past purchases 
      List<PurchasableProduct> products = [];
      StoreState storeState = StoreState.loading;
    
      bool isPaidVersion = false;
    
      PaidVersionCheckNotifier() {
        iapConnection_subscribeToPurchaches();  // subscribing to purchaces, made during runtime
        iapConnection_loadProducts();           // loading available products
    
        iapHelper_initialize();                 // initializing the helper  
        iapHelper_getSubscriptionHistory();     // querying past purchases. 
      }
    
      void iapHelper_initialize() async {
        await FlutterInappPurchase.instance.initialize();
      }
    
      void iapConnection_subscribeToPurchaches() {
        final purchaseUpdated = iapConnection.purchaseStream;
        _subscription = purchaseUpdated.listen(
          _onPurchaseUpdate,
          onDone: _updateStreamOnDone,
          onError: _updateStreamOnError,
        );
      }
    
      @override
      void dispose() {
        _subscription.cancel();
        FlutterInappPurchase.instance.finalize();
        super.dispose();
      }
    
      Future<void> buy(PurchasableProduct product) async {
        final purchaseParam = PurchaseParam(productDetails: product.productDetails);
        switch (product.id) {
          case storeKeyUpgrade:
            await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
            break;
          default:
            throw ArgumentError.value(
                product.productDetails, '${product.id} is not a known product');
        }
      }
    
      Future<void> _onPurchaseUpdate(
          List<PurchaseDetails> purchaseDetailsList) async {
        for (var purchaseDetails in purchaseDetailsList) {
          await _handlePurchase(purchaseDetails);
        }
        notifyListeners();
    
        iapConnection_loadProducts();
      }
    
      Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
        if (purchaseDetails.status == PurchaseStatus.purchased) {
          switch (purchaseDetails.productID) {
            case storeKeyUpgrade:
              isPaidVersion = true;
              notifyListeners();
              break;
          }
        }
    
        if (purchaseDetails.pendingCompletePurchase) {
          await iapConnection.completePurchase(purchaseDetails);
        }
      }
    
      void _updateStreamOnDone() {
        _subscription.cancel();
      }
    
      void _updateStreamOnError(dynamic error) {
        //Handle error here
      }
    
      Future<void> iapConnection_loadProducts() async {
        final available = await iapConnection.isAvailable();
        if (!available) {
          storeState = StoreState.notAvailable;
          notifyListeners();
          return;
        }
    
        // Fetching product details
        const ids = <String>{
          storeKeyUpgrade,
        };
        final response = await iapConnection.queryProductDetails(ids);
        products =
            response.productDetails.map((e) => PurchasableProduct(e)).toList();
    
        storeState = StoreState.available;
        notifyListeners();
      }
    
      void iapHelper_getSubscriptionHistory() async {
        final available = await iapConnection.isAvailable();
        if (!available) {
          storeState = StoreState.notAvailable;
          notifyListeners();
          return;
        }
    
        final purchaseHistory =
        await iapHelperConnection.getPurchaseHistory().catchError((error) {
          // Handle any errors that might occur
          debugPrint("Error fetching purchase history: $error");
          return [];
        });
    
        debugPrint("In-app purchase history:");
    
        // Assuming purchaseHistory is a list of purchases
        // Iterate through each purchase and print details
        if (purchaseHistory != null) {
          for (var purchase in purchaseHistory) {
            debugPrint(
                "Purchase ID: ${purchase.productId}, Product ID: ${purchase.transactionDate}");
            // Add more details as needed
          }
    
          if (purchaseHistory.any((p) => p.productId == '<my product id I am looking for>')) {
            isPaidVersion = true;
            notifyListeners();
          }
        }
      }
    }