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