Detecting refunds in in_app_purchase

I have successfully implemented in-app purchase in my Flutter app. The one scenario I am having difficulty with is refunds. How do I detect if the user received a refund on a purchase?

I see discussions like this (from three long years ago!) which mention functions like InAppPurchaseConnection.instance.queryPastPurchases() but (a) there is no InAppPurchaseConnection class (it’s now InAppPurchase?) and (b) there is no queryPastPurchases function to be found.

Is there a function I can call from the app? Maybe I can call InAppPurchase.restorePurchases() and if it returns an empty List that means no purchases are still valid? That’s what it appears to be doing on an Android device, though this wasn’t working for me earlier on an emulator. (It insisted on restoring the refunded item.)

Okay, putting some of the pieces together. It looks like queryPastPurchases() is on the Android side of things. If you create an InAppPurchaseAndroidPlatformAddition you can call queryPastPurchases and apparently get a list of the currently valid purchases. (Refunded purchases are removed from the list.) I’m curious if there is anything analogous on the iOS side, but I don’t have to worry about that immediately.

Here is some code. (You have to specifically add in_app_purchase_android so your project knows about InAppPurchaseAndroidPlatformAddition.)

if (Platform.isAndroid) {
  final addition = InAppPurchase.instance.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
  addition.queryPastPurchases().then((QueryPurchaseDetailsResponse response) {
    final productIds = response.pastPurchases.map((GooglePlayPurchaseDetails details) {
      return details.productID;
    });
    if (!productIds.contains('the_product_id')) {
      // never purchased or refunded after purchase
    }
  });
}

Now I’m wondering if this accomplishes anything different than the InAppPurchase.restorePurchases() I mentioned previously. Would there be a downside to calling restorePurchases on every app start?

I would recommend you to use purchases_flutter | Flutter package (RevenueCat).

You’ll have more tools to work with this, your purchases will be server-side confirmed and, as long as you don’t earn 1 million per year, it is completely free.

Google’s InAppPurchase package is pure crap (especially for subscriptions).

Source: me, who have to deal with that shit for years for thousands of users.

PS: RevenueCat is not without flaws: sometimes, 1 or 2 users complain about paid subscriptions that are not confirmed (about 0.1%). The good thing is that I can log in RevenueCat backend and grant subscriptions to those users manually.

How do I detect if the user received a refund on a purchase?

The most effective way (I’d even say, the only correct way) is to do it on the backend side.

Your backend should access the IAP Play API and query it, and also listen for any changes pushed via pub-sub/websocket connection from Google Play to your backend.

The backed will get events when payments renew, users cancel, refund etc. And then you update your database accordingly.

The backend can also query the status of a subscription at any time using the Play API.

It looks like that’s changed. It’s free if you earn less than $2500/month (30K/year) and 1% after that.

I’ll do it if I have to, and I imagine the 1% could very well be worth it, but I don’t like having that extra layer. I implemented a client’s iOS app using RevenueCat and when things went wrong it was frustrating trying to figure out where the problem was, in the App Store or on RevenueCat. And my needs are rather simple, so I don’t feel I need a lot of the fancy features RevenueCat has.

Thank you for your reply! I’m really green here so need to hear what best practices are.

Did you implement your backend from scratch?

Or is it more common to use something like Firebase here? What do the costs of that look like after a while?

Another thing I’ll mention is I don’t want my users to have to log in. That makes using a backend to verify and keep track of purchases difficult, doesn’t it? (This is one area iOS has going for it, some basic CloudKit stuff accessible, over all the user’s devices, without the user having to log in.)

Implementing In-App-Purchases is 90% backend work, 10% app work.

You absolutely want to have a backend handling the communication between your company and Google Play. Just because the device app approved the purchase doesn’t mean the purchase succeeded, you actually need to take the receipt you get from the IAP library, give it to your backend, and then it should verify the purchase against the Apple or Google API.

Even when users are “anonymous”, you still want to track purchases somehow, right?

The Flutter team has a codelab on how to implement in-app-purchases that shows some of the backend work to handle consumables and subscriptions: Cómo agregar compras directas desde la aplicación a tu app de Flutter  |  Google Codelabs

I am honestly not sure if doing “backend-less” in-app-purchases is even feasible, maybe tools like Revenue Cat can help with that. But at the end you are paying Revenuecat so they do the backend for you.

I think it’s feasible if one is not overly concerned with fraud. (Which I am not at this point.) Google Play tells you when a purchase is successful; enable functionality. Query Google Play on subsequent start-ups as to what the user’s purchases are; disable functionality as appropriate. And no need for the user to create a new login as it’s using the stores’ login under the covers.

Incidentally, I found an explanation for the disappearance of InAppPurchaseConnection.queryPastPurchases:

InAppPurchaseConnection.queryPastPurchases was removed in favor of InAppPurchase.restorePurchases.

I still prefer InAppPurchaseAndroidPlatformAddition.queryPastPurchases(), in that it returns to me a list of purchases rather than have to wait for a future InAppPurchase.purchaseStream event. But calling InAppPurchase.restorePurchases seems to be the expected method to get what I want.

but I don’t like having that extra layer

Either that or you’ll have to implement and maintain two different backends.

Google Play, for instance, will have a pub/sub for subscriptions/purchases changes. You’ll have to listen to it and update your backend. Apple do whatever apple do, and I’m pretty sure is complicated, alien and doesn’t work properly (as anything apple do).

I agree about what you said 100%, but, sometimes, things are a bit more complicated than expected.

BTW, consider this scenario:

An user purchases something. Your client-side app will say “ok. he/she purchased it, I’m granting it to he/she”. Then, user ask for a refund. Google won’t tell your client-side app this. It will publish in a pub-sub channel. So, you’ve been bamboozled.

Subscriptions are worse: That package doesn’t even say how much time the subscription will last (a week? a month? a year?). You have to assume.

1 Like

sometimes, things are a bit more complicated than expected

Oh, certainly! I’m not ruling out giving up and using RevenueCat. (I don’t see myself maintaining my own server.) Just not yet. :slight_smile:

user ask for a refund. Google won’t tell your client-side app this

I’m going to try having my app ask for a “restore purchases” once a day. It appears on refund, the purchase disappears from the list. I hear that Apple may throttle one with too many of these calls, but it’s kind of vague.

Subscriptions are worse: That package doesn’t even say how much time the subscription will last

Presumably one could calculate the length of the subscription given the product ID and the transaction date? Not sure how soon I’m going to jump into subscriptions.

And once I’ve played around with iOS in-app, I’ll try to update this thread with what I find.

Presumably one could calculate the length of the subscription given the product ID and the transaction date? Not sure how soon I’m going to jump into subscriptions.

Sure.

But, answer this: what is a month for you?

February 2 plus 1 month equals March 2 seems right to you? For me, it does. But for Apple and Google the subscription is 1 logical month or 30 days? I have to think, I have to consider leap years, I have to consider timezones. I don’t want to waste time thinking in a problem that is not mine. And those fuckers are getting a 15% cut of my money (that’s 3x more than a credit card company would charge to use their payment method!!!).

Played a little with in-app purchases on iOS. The pain point right now is that there doesn’t appear to be any way to determine if the user was refunded on an in-app purchase. When calling “restore purchases”, all purchases (refunded or not) are returned with a status of “restored”. Not helpful. Nor is anything in in_app_purchase_storekit.

That said, I found this in “What’s new in Flutter 3.27” hopeful!

We’ve added StoreKit 2 support to the in_app_purchase_storekit package to migrate off StoreKit 1 APIs, which were deprecated in iOS 18. This allows us to add new StoreKit 2 features like better subscription management in the future.

Better subscription management… :drooling_face: