Flutter in-app Purchase
Quick Q&A for Seamless In-App Purchases
For most of the app, in-app purchases will be on the roadmap sooner or later. As a Flutter developer, implementing in-app purchases for both iOS and Android can be quite frustrating because of Apple and Google policies. You give more time to set up and understand the flow than writing actual code.
So in this article, I will cover as many questions from start to production (except those that are easily available in the documents) and some common problems I faced and how I solved them. It will be in a Q&A format so you can find a question that is bothering you and hopefully find a satisfying answer.
PS: I will be focusing on Consumable Purchase in this article
General Q&A on the Mobile Side
Most Important Step
Before you start any of the in-app purchase stuff, even before start doing research or even adding a package on your dependencies, clear out all tax details and financial information in the respective consoles (Apple and Google) because it will take some working days and without that, things will not work in app and you will wander around thinking what mistake you did.
What are the types of in-app purchases?
- Consumable — Which can be purchased many times (eg: coins in games)
- Non Consumable — Which remains for a lifetime after a purchase
- Subscription — Which charges the user periodically
Which Flutter Package to use
I used this package, in_app_purchase which is from the official Flutter team, in my experience I found some issues that are still open on their GitHub which is pretty annoying. So if you are in the phase of researching the package do have a look at other alternative and their GitHub issues.
(iOS) I have created products in app-store, filled all detail but the status is still in Missing Metadata
There is one optional field inside the product detail page in iOS and it is a screenshot, so you need to upload a ScreenShot with the purchase bottom sheet, and the status will be moved to the next step.
But, How to add a ScreenShot of the app if you can’t fetch and start purchase? It will be answered in the testing in-app purchase section.
TLDR you can use StoreKit in Xcode.
I have created products on AppStore and Google Play console, but when I query in the app why the product won’t appear?
- Check if all financial forms like Tax Details, Paid Application Agreement, etc are settled out.
- Check for the status of Products, it should not be on the Missing Metadata state
- Check for the product name during a query, there can be some typos.
- If you have used flavor in the app, make sure the package name exists in the respective console.
- Some old Android devices might not support Android billing API. If this results in false, then there might be some issue on your device
await InAppPurchase.instance.isAvailable();
- Prefer using physical devices rather than emulators and simulators as much as possible.
(iOS) in-app purchase testing
Now you can query and obtain the products and initiate purchase, but how to test it without using actual money? iOS offers us a generous way to test those, let's understand them one by one.
1. StoreKit
StoreKit allows you to simulate purchases in your local environment (Xcode). You can create a StoreKit file from Xcode and either link it with your developer console to fetch the products or even create products there manually.
Once you create a StoreKit file, any purchase made will be instantly reflected on your Xcode, you can simulate failures, delete purchases and much more locally.
Even after creating StoreKit file purchases are not simulated, why?
You might face this issue in such a case, open edit schema in XCode and make sure to select the StoreKit file as below.
2. Sandbox
StoreKit is used to simulate purchases in your local environment but if you want your team member or testers to test in-app purchases you have to use Sandbox.
Sandbox provides us with a way to test in-app purchases like in the real scenario but, without making an actual purchase. Add an email to Sandbox like below and in your iPhone.
Now onward on this device if you try to make a purchase it will go through this sandbox email. During each purchase app will ask you to enter the sandbox email and password, and after it is verified you are good to go.
3. TestFlight
TestFlight is the easiest way to test in-app purchases. Internally it will create your sandbox account and handle everything you did above, you can use your own AppleId to do all the work. So the purchase made using the TestFlight version will also not make the actual purchase.
(Android) in-app purchase testing
1. Licence testing
For Android it's simple, You will need to add the list of users and their emails, and purchases made from those emails will not be charged. Make sure the device has PlayStore logged in with that email.
Simple it is :)
We have our own server verifying purchases and assigning the purchase value to users what else should be handled on the mobile side?
It is common practice to verify purchases on the server side, the basic flow will be.
Mobile Initiating Purchase → Confirmation from Store → Request Server to verify and consume purchase (involves assigning purchase good to user account etc) → Acknowledge to mobile → Mobile will ack Store that purchase is completed.
Server-Side Verification Flow
After completing the purchase from respective stores the purchase information should be sent to the server to validate the purchase and redeem the product of those purchases to the user account in the form of coins etc.
The server will play a role in validating purchases and preventing fraud, duplicate redemption, etc.
What should be the General Flow with server verification?
- Initiate purchase with store
- If the purchase is successful request the server to verify the purchase
- The server will ACK client about the purchase. If the purchase is successful server will credit the value to the user's account.
- The client should mark the product as completed with the code below. This is an important step that the client should not miss.
/// It will inform underlying store that purchase is completed and consumed.
await InAppPurchase.instance.completePurchase(purchaseDetails);
What information mobile should send to the server to verify the purchase?
final verifyPurchaseDTO = VerifyPurchaseDTO(
store: Platform.isIOS ? appleStore : androidStore,
purchase: PurchaseEntity(
productId: purchaseDetails.productID,
serverVerificationData:
purchaseDetails.verificationData.serverVerificationData,
),
);
What if the purchase from the store was completed but the mobile didn’t get Positive ACK from the server?
In this case, the client will not have completed the purchase i.e. completePurchase
was not called.
So, as per the documentation of the package, in the next app session purchaseStream
will be triggered with those pending products and you can try again to communicate with the server, verify the purchase, and mark it as completed.
/// If a purchase is not completed ([completePurchase] is not called on the
/// purchase object) from the last app session. Purchase updates will happen
/// when a new app session starts instead.
/// This stream will be triggered with the last purchase information
Stream<List<PurchaseDetails>> get purchaseStream =>
InAppPurchasePlatform.instance.purchaseStream;
General Issues
In iOS if I open the Purchase Sheet and close it, purchase items are blocked
Only for iOS, you need to perform the following things
final instance = InAppPurchase.instance;
final Stream<List<PurchaseDetails>> purchaseUpdated =
instance.purchaseStream;
_subscription = purchaseUpdated.listen(
_listenToPurchaseUpdated,
);
void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.canceled) {
/// Here it is handled
if (Platform.isIOS) {
await InAppPurchase.instance.completePurchase(purchaseDetails);
}
}
}
}
My all products are in pending mode(blocked) and I am not able to make purchase requests. How to free those?
This can generally happen during development mode, our server will also be developed in parallel so chances are there can be a bug on either side that will lead to the product not being consumed successfully due to which the item will stuck in pending mode and the user can do nothing.
In such cases, you can free them up with a simple hack.
At the start of the app try to restore the purchase.InAppPurchase.instance.restorePurchase()
It will re-trigger InAppPurchasePlatform.instance.purchaseStream
and you can mark it as completed withawait InAppPurchase.instance.completePurchase(purchaseDetails);
If you are using StoreKit for iOS you can easily delete those from Xcode and continue.
When should you subscribe to the purchase stream?
It is generally recommended to subscribe as soon as your app launches because if you subscribe to this stream to some inner screens and if the last purchase failed, the next time the user opens the app he might not get this stream triggered to handle the failure case.
So it's always good to handle early.
I wrote this article from all my learnings and mistakes during the implementations so there can be some mistakes or something missing.
If you have any questions please ask in the comment and I will try to update the article with that query.
If you want to contribute to this article from your learnings please comment and I will reach out to you, my intention is this article will cover as much as queries regarding this topic.
See you soon in next :)