Skip to content

[in_app_purchase] Support InApp purchase local verification #52522

Open

Description

@rahulraj64

InApp purchase plugin should support local/offline purchase verification
Android Purchase Verification:
https://developer.android.com/google/play/billing/billing_library_overview#Verify-purchase-device
iOS Receipt Validation: https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html

This is not only a security enhancement but also a functional bug fix. I can explain why.
In Android, when we purchase an auto-renewing subscription, the plugin returns that subscription when we query the past purchases and when the subscription is expired or the user cancels it, the plugin will not return the purchase when we query it. So in Android, this works as expected. But in iOS there is a behaviour change when the subscription is expired. The plugin still returns the purchase when we query the past purchase, which is unexpected in the perspective of the plugin user, especially if he successfully implemented the Android part and according to the user, this is a bug in the plugin.

This may be a problem of the iOS ecosystem that the validity of the receipt may be checked locally in order to determine whether the subscription is active or not. But since this is a plugin, it should abstract away all of the complexities like this and provide idiomatic APIs to handle the situation like this.

So the plugin should provide some basic offline verification of the purchase for security and functional consistency.

Tested in in_app_purchase: 0.2.0+7

Activity

  1. LHLL commented on Jun 17, 2020

    @LHLL

    Please see Apple document for restoring transaction part, quote here:
    You can sync and restore non-consumables and auto-renewable subscriptions across devices using StoreKit. When a user purchases an auto-renewable or non-renewing subscription, your app is responsible for making it available across all the user's devices, and for enabling users to restore past purchases
    If your app is selling subscription on iOS, it doesn't matter whether or not a subscription is expired, as long as it's a valid purchase, Apple will restore this transaction for you.

    Local receipt validation only provides:
    1. If a receipt is signed by Apple.
    2. If a receipt belongs to this device.
    3. If a receipt belongs to this app.

    Comparing system time with receipt locally to determine whether or not a subscription is expired will never work, since user can adjust iOS system time manually.

  2. rahulraj64 commented on Jun 17, 2020

    @rahulraj64
    Author

    Hi @LHLL
    Thanks for your input. I understand that it is not safe to validate the receipt locally. But what about providing a minimal support for parsing the receipts and providing the human readable properties to the developer? Some fields like (https://developer.apple.com/documentation/appstorereceipts/responsebody/latest_receipt_info). And let the developers take a decision on whether they wanted to trust the receipt or not (say, if there is no connectivity, use offline verification, otherwise online verification, depending on their use case. In my case, for auto renewable subscription, if the user adjust the system clock while offline, its okay since they are viewing irrelevant content inside app). This helps to create apps with full offline support.
    Please correct me if I am wrong.

  3. odonc commented on Jan 9, 2021

    @odonc

    Hi @LHLL
    Thanks for your input. I understand that it is not safe to validate the receipt locally. But what about providing a minimal support for parsing the receipts and providing the human readable properties to the developer? Some fields like (https://developer.apple.com/documentation/appstorereceipts/responsebody/latest_receipt_info). And let the developers take a decision on whether they wanted to trust the receipt or not (say, if there is no connectivity, use offline verification, otherwise online verification, depending on their use case. In my case, for auto renewable subscription, if the user adjust the system clock while offline, its okay since they are viewing irrelevant content inside app). This helps to create apps with full offline support.
    Please correct me if I am wrong.

    Agree with this, let us take on the risk! Something to parse the receipt would be great, I can't seem to work out how to parse that ANS1 string.

  4. renefloor commented on Mar 25, 2021

    @renefloor
    Contributor

    I'm not sure if I agree that local verification should be supported.

    First of all:

    say, if there is no connectivity, use offline verification, otherwise online verification

    To have an up to date receipt you need a network connection. So the offline verification will be as good as the last verification when the device was online. At that moment online verification was possible. This sounds like you would want a way to store the result of the online verification. I would recommend using flutter_secure_storage for that.

    When verifying the receipt on the device Apple recommends (among other things) to

    • Using a static library of OpenSSL (not a dynamic library) or a custom implementation to verify the signature.
    • Ship the certificate with the app (which means updating the library regularly for the certificate to prevent expiry) or download it from the app, which means we need a caching mechanism to make it offline possible.
    • You should use hardcoded values for bundle version and identifier and not put it in Info.plist. I'm not fully sure if hardcoding it in dart and sending it on a methodchannel would be recommended (I don't think so).

    I don't see enough benefits and to many risks implementing local verification. I think it might be better to create an example cloud functions app that does this online.

    I can recommend this video on local and server receipt verification: https://developer.apple.com/videos/play/wwdc2017/305

  5. ludwiktrammer commented on Mar 26, 2021

    @ludwiktrammer
    Contributor

    My use case is as simple as this:

    queryProductDetails when used with subscriptions returns only active (not expired) subscriptions on Android, but returns all (even expired) subscriptions on iOS. I want to perform server verification, but I would like to be able to filter out the expired purchases first, locally, and only send the active subscription for the online verification. I don't see a way to do this currently.

  6. renefloor commented on Apr 7, 2021

    @renefloor
    Contributor

    Seeing the use cases this sounds like a duplicate of #43720

  7. added
    P3Issues that are less important to the Flutter project
    on Apr 29, 2021
  8. renefloor commented on May 11, 2021

    @renefloor
    Contributor

    I had a thorough look into receipt verification. I think it is possible to do in pure dart, but it would be really nice if somebody with more asn1 decoding experience can give a helping hand.

    This is a gist of some example code that prints all the asn1 objects in the receipt: https://gist.github.com/renefloor/9be7aecec1005779b3878e008b335d64

    You can use the example receipt in this online parser to see how the structure actually is.

    There are 2 element tags that are not recognised by either parser, 160 / 0xA0 and 163 / 0xA3, but I chose to just parse the bytes in those objects.

    This is a really useful example in Swift which helps understanding how things work: https://github.com/roop/AppStoreReceiptDecoder/blob/master/AppStoreReceipt.swift https://github.com/roop/AppStoreReceiptDecoder/blob/master/main.swift

    The official documentation is really technical, but at least these two pages are really helpful in combination with the swift example:
    https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW19
    https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1

  9. espbee commented on May 2, 2022

    @espbee

    @renefloor thanks for this info. have you been doing such validation / come to any new conclusions ?

  10. 1 remaining item

  11. added
    ⏳Bot is counting down the days until it unassigns the issue
    on Nov 23, 2023
  12. flutter-triage-bot commented on Nov 23, 2023

    @flutter-triage-bot

    This issue is assigned to @LHLL but has had no recent status updates. Please consider unassigning this issue if it is not going to be addressed in the near future. This allows people to have a clearer picture of what work is actually planned. Thanks!

  13. flutter-triage-bot commented on Feb 1, 2024

    @flutter-triage-bot

    This issue was assigned to @LHLL but has had no status updates in a long time. To remove any ambiguity about whether the issue is being worked on, the assignee was removed.

  14. removed
    ⏳Bot is counting down the days until it unassigns the issue
    on Feb 1, 2024
  15. moved this from To triage to Moderate, High Priority in Storekit 2 Tracker/[in_app_purchase_storekit] trackeron Dec 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Issues that are less important to the Flutter projectc: new featureNothing broken; request for a new capabilityc: proposalA detailed proposal for a change to Flutterp: in_app_purchasePlugin for in-app purchasepackageflutter/packages repository. See also p: labels.team-ecosystemOwned by Ecosystem teamtriaged-ecosystemTriaged by Ecosystem team

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions