Search Unity

[FEATURE REQUEST] Differentiate between new purchases and restored purchases

Discussion in 'Unity IAP' started by josephsaade, May 29, 2019.

  1. josephsaade

    josephsaade

    Joined:
    Apr 18, 2016
    Posts:
    46
    In the current implementation, any IAP goes to the IStoreListener function ProcessPurchase
    .
    There is no differentiation being made between a new purchase and a restored purchase.
    So if we send any analytics to a 3rd party platform, non-consumable purchases are being reported twice as the 2nd time happens when the IAP is restored.

    In our naive solution, we tried to set a flag when the user clicks on an IAP button so we know that this IAP is actually being purchased and not restored.

    But this fails, as on iOS, if the user clicks on the button, he can be prompted by Unity to "restore" that purchase for free and we as developers do not have any knowledge or callback that this IAP has been restored and is not a new one.

    Please advise.
     
  2. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    Restore on iOS is a separate method, and requires a separate button click.
     
  3. josephsaade

    josephsaade

    Joined:
    Apr 18, 2016
    Posts:
    46
    Hi Jeff, I am not sure that you have understood my issue.

    iOS Restore is a separate method and I agree, but there is a specific scenario:
    - User does not click on "Restore" button on iOS
    - User clicks on non-consumable IAP that he has previously purchased.
    - Unity IAP prompts the user that a restore will happen and if he wants the item for free, Unity IAP sends me a call to process the purchase but I do not have any way of knowing that this purchase has been restored and is not a new purchase.
     
  4. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    A user should not be able to click on a Non-Consumable button if they have already purchased it. Your UI should not allow this in the first place as the product should already be available for them. How are you tracking and locally persisting user purchases? PlayPrefs is one way.
     
  5. josephsaade

    josephsaade

    Joined:
    Apr 18, 2016
    Posts:
    46

    On iOS, if a player removes and re-installs the game, we do not know that he already purchased the non-consumable until he clicks on "Restore Purchases". So if he clicks on the non-consumable before clicking on "Restore Purchases" after a new install, the above scenario triggers.
     
  6. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    You would want to track purchases using persistent storage, or in the cloud. This is a feature we are considering adding, but no time frame
     
  7. josephsaade

    josephsaade

    Joined:
    Apr 18, 2016
    Posts:
    46
    Currently the Unity IAP is prompting the user that this is a free purchase, It would be great if they send us (as developers) that info too so we can manage it internally.
     
  8. technobayo

    technobayo

    Joined:
    Apr 8, 2017
    Posts:
    13
    Hi @josephsaade ,

    I have a similar situation and here's how I'm handling it. In my case, a user is first authenticated before making any purchase. When the 'Buy' button is pressed, I call a method to check my database if the user already has the item. If the user has the item in the database then I simply prompt the user to use the 'Restore' button. This way, I bypass Apple's prompt.
     
  9. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    That is not a good solution. You should not be presenting a Buy button to the user if they can't actually make a purchase. Please see my previous post. You are correct, PlayerPrefs can be removed during reinstall, that is why many studios opt for saving purchases to the cloud. If they already made a purchase, then don't enable the Buy button in the first place. Don't wait for them to click it.
     
    technobayo likes this.
  10. technobayo

    technobayo

    Joined:
    Apr 8, 2017
    Posts:
    13
    Thanks for your response. My app allows for multiple purchase channels so it's not restricted to the App Store. That said, the login feature is optional for the users when starting the app. Thus, a user who previously made a purchase via the App Store may upon reinstalling the app chose not to login. As I've experienced times without number with my users, they often press the buy button again instead of restore. In that case, it triggers the sign-in panel so that I can authenticate their previous purchase before removing the locks and buy button.
     
  11. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    IAP does not use your login process if that is what you are referring to (if you have a custom login), it uses whomever is logged into Google Play on the device. You should not present a Buy button to a user if they are not able purchase, it's not a good user experience. You would need a way to track the purchases. We are looking into an inventory management solution, but no ETA.
     
  12. bourriquet

    bourriquet

    Joined:
    Jul 17, 2012
    Posts:
    181
    I would also need a way to know in ProcessPurchase if the product has already been purchased and is being restored or not.
    It's important for receipt validation, because if the purchase is being restored, the transaction ids don't match between the one provided in PurchaseEventArgs.purchasedProduct.transactionID and the one in the decrypted receipt from PurchaseEventArgs.purchasedProduct.receipt. This will be interpreted as an invalid receipt if we don't know that it's a restore.
     
  13. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    We use the Google purchaseToken and not the transactionID. We've looked at this issue previously and the Google API does not differentiate. Hopefully with Google Play Billing library v4 and v5 they may improve on this.
     
  14. bourriquet

    bourriquet

    Joined:
    Jul 17, 2012
    Posts:
    181
    I'm actually working on the iOS side for now.
    How does that work there?
     
  15. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    I believe we use the transactionID on iOS. What is your use case?
     
  16. bourriquet

    bourriquet

    Joined:
    Jul 17, 2012
    Posts:
    181
    In ProcessPurchase, I send the purchase data (bundleId, transactionId, receipt...) to a remote receipt validation server. The server decrypts the receipt and compares the info.
    When restoring (or re-buying), the transactionIds don't match, so the validation fails.
     
  17. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    Keep in mind that Apple bundles all their receipts. We are working to incorporate StoreKit2 where the behavior may change, so we aren't expecting any changes in this area until then.
     
  18. bourriquet

    bourriquet

    Joined:
    Jul 17, 2012
    Posts:
    181
    I'm confused. So what are we supposed to do? Should I just not compare transaction IDs?
     
  19. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    The Apple payload may contain multiple receipts, I was just letting you know. So you need to make sure you are using the right one, typically the one with the most recent date. But I believe the TransactionID might change with each subscription auto-renew for that particular receipt. Again, something you'll want to check.
     
  20. bourriquet

    bourriquet

    Joined:
    Jul 17, 2012
    Posts:
    181
    Nope. The payload only contains one receipt. And it's the receipt of the original purchase. With the original transaction ID. Which doesn't match the one provided by ProcessPurchase.
     
  21. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
  22. abcjjy

    abcjjy

    Joined:
    Mar 6, 2015
    Posts:
    35
    This only works for user initiated restore purchase, where orginalTransactionIdentifier == transactionID or originalTransactionId == null means new purchase.

    However, when app is newly installed and receipts are refreshed, purchase callbacks are triggered for non-consumables. In this case, the transactionID can't distinguish new purchase from restore.

    After exploring some source code, I found two parts relevant to this issue. The AppleStoreImpl adds recept data to product and the PurchasingManager.ProcessPurchaseOnStart reads receipt data from product object and triggers callback. ProcessPurchaseOnStart is convenient in some situations, but it confused store api callback and restored purchase from receipt. It is better to add a parameter to indicate the source of the purchase: store callback or receipt.
     
    Last edited: May 20, 2022