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?

1 Like

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.

1 Like

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.

2 Likes

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:

Don’t want to be demotivating, but I think that trying to implement in-app purchases client side only is the best lesson to never try it again without backend :wink: We used Revenuecat for a while and eventually dropped it once we had our own backend to support subscriptions. There are also ways to deploy set of cloud functions to Firebase to handle purchases in the simplest form possible.

3 Likes

I am in the same position now: need to get IAP for subscriptions on iOS going. Holy crap is that complex ! What are you using today ?

Can you describe a bit in detail where you struggle? I’m currently implementing this too.

Biggest problem is currently the Apple Sandbox environment. I was going nuts about not being able to download any products, despite everything configured correctly on the AppstoreConnect side.

I was using RevenueCat with the hope of easier integration into the App and server-side verification yadiyadiya. Didn’t work either. So I implemented a parallel in-app-purchase plugin pay-system where I can easily switch between either RevenueCat or in-app-purchase. I want to know, where the problems I have stem from, so if I use in-app-purchase, then communication goes straight to Apple, whereas using RevenueCat flutter plugin, communication goes mostly to RevenueCat, partly to Apple.

As both didn’t work, the problem needed to be with the communication to Apple. It’s really interesting, that signaling / error propagation is absolutely none here, so it’s really hard to find the real reason for a defect behaviour. In my case, fetching the products simply blocked endlessly. I was using a timeout block around the call, because both plugins don’t support setting any timeout values.

Finally, after restarting my phone (!) everything started to work. Sorry, if this gets a bit longer here …

The real fun on my end is the detection logic for product cross-grades in the Sandbox environment …
So we have this monthly and yearly subscription. Both are placed at the same priority in AppStore Connect, i.e. switching between these is effectivley a cross-grade. In the sandbox environment (not the one from XCode, I mean the AppStore sandbox), monthly subscriptions come in after 4-5 minutes.
In case of purchasing a yearly subscription after the monthly subscription, first a pending purchase arrives and theoretically when the new subscription activates a “purchased” status of this product arrives. For the transition period, I am tagging the newly purchased subscription “pending”. This is also clearly visible to the user with exact dates, etc. so that he/she can easily grasp the current subscription and when the new purchase gets active.
But the sandbox sends a few seconds later already a “purchased” event and even doesn’t wait until the current subscription ends. This wrongly activates the yearly subscription and tags the monthly purchase as available to be cross-graded to.
My current work-around is to ignore any “purchased” event from the AppStore for pending products up-to 1 minute after a purchase, if a currently active subscription exists. This heuristics seems to work very well, but now I am diving into the “restore purchases terrain”, and I am absolutely not sure, how this above scenario can be detected here for the sandbox.

What I find really interesting is that most code I find related to in-app-purchase is absolute demo beginners-code that doesn’t come close to production-grade solutions. What if the network is not available ? What if suddenly the network becomes available ? What about the above concrete (and quite common) scenario ?

I ended up with an architecture, where I clearly separate the service, have an own PayStore, PayXXXRepositories, PayScreen with PayScreenCards for each product, etc. where online-offline scenarios are built-in and where the single source of truth is the PayStore and never the service. My PayService is abstracted so I can easily switch between different service providers.

What holds me off from RevenueCat, besided being another point of failure between my App and Apple is the vendor lock-in. I’d use it only for server-side receipt verification, so I am really thinking about rather self-hosting a game server like Nakama, which already has built-in AppStore/PlayStore, etc. receipt validation and where you can do a lot more yourself (if you want to).

Coming back to the IAP integration: choosing the underlying service is IMHO an important but minor part of the implementation. Most business logic stems from the complexity of IAP requirements of the AppStore itself and the production-grade complexities related to make the purchase flows reliable for every typical scenario.

Are you trying to do that without your own backend?

Yes. My approach is to get as far as it gets without a backend. And only use the backend, if it’s absolutely necessary. I understand, that cancellations of subscriptions are probably the hardest to detect without a backend. Any ideas here ?

It has been a while since I last solved this problem for subscriptions in a project of my own but from what I recall the core reasons for having a backend play a role in payment processing / subscription management are:

  • you can’t rely on all the data associated with a transaction being available in subsequent sessions
  • you are trusting the client to say that they have paid. This leaves the door open for a bad faith client to get a free subscription

I have limited experience with RevenueCat but if you are using that service I imagine that they are performing the tasks that a custom backend might be performing on your behalf - validating the transactions server side.

2 Likes

There a few revenuecat alternatives with free tiers.

packages:
6k downloads

1k downloads

800 downloads

200 downloads

I didn’t try them or implemented in app purchases. I was just looking for something that is open source or self hostable. Sadly I can’t find anything. Wonder why?

edit: found an open source project, seems like it’s in early development GitHub - ProjWildBerry/wildberry: Inspired by the need for freedom in app monetization. Thanks to the open-source community for their invaluable contributions to software freedom.