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

1.4k views Asked by At

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.

2

There are 2 answers

0
ForestG On BEST ANSWER

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

1
AppSolves On

When I started using the in_app_purchase package, I was frustrated too. It was my very first app, so I didn't want to build an entire backend and also didn't want to spend too much money for my first project.

I looked for dozens of tutorials on YouTube but all I found were outdated videos using deprecated APIs. I also came across alternatives such as RevenueCat, but I was too concerned of giving access to financial data to a third-party service.

I really do think it would be better to verify in-app purchases on your backend. You could keep track of the purchases and restrict access to your premium features if transactions are voided/refunded. And users could - as stated in the documentation - rewind their system clock so subscriptions would never expire. On the other hand, how many regular users would make the effort to always change system settings only because they want to save 1,99$ for a good app?

But now back to your question: It is only a recommendation, so as long as you want to save yourself the trouble, I think you can just verify it from the client side (it should be relatively secure). I assume you already got to know Google's Android Publisher API during your research. See here for more info. Just make a http request with your app's packageName, the purchase's productId and the token (provided in the package's PurchaseDetails class) and the endpoint will give you details about the purchase. You can also use the googleapis package for that.

Good luck with your project!