Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

IAP receipt validation on app start

Discussion in 'Unity IAP' started by pawadski, Jul 12, 2020.

Thread Status:
Not open for further replies.
  1. pawadski

    pawadski

    Joined:
    Apr 13, 2020
    Posts:
    12
    Hello,

    What is the preferred method to deal with ensuring the user still has the purchase active on app start? Should I save the product receipt to disk and load it up on app start? Should IAPListener fire a purchase event automatically on app start?
     
  2. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
  3. pawadski

    pawadski

    Joined:
    Apr 13, 2020
    Posts:
    12
    Thanks. The sample iap project does not store receipts on disk, correct? I see messages in my Console when I start the app:
    <i>AndroidPlayer(ADB@127.0.0.1:34999)</i> Initializing UnityPurchasing via Codeless IAP
    <i>AndroidPlayer(ADB@127.0.0.1:34999)</i> UnityIAP Version: 1.23.3
    <i>AndroidPlayer(ADB@127.0.0.1:34999)</i> UnityIAP: Promo interface is available for 1 items

    But OnPurchaseComplete of my IAP button or IAP listener are not called on start. Should they be automatically or should I do something on start? Google Play only by the way.

    The IAPListener component is attached to an object that is present in the first scene and is not destroyed in scene transitions.
     
    Last edited: Jul 18, 2020
  4. SamOYUnity3D

    SamOYUnity3D

    Unity Technologies

    Joined:
    May 12, 2019
    Posts:
    600
    I suggest you use the script IAP to get previous purchases. For non-consumables and subscription products that have been purchased, you can get them in IStoreListener.OnInitialized.

    Below is the code from IAP demo scene, you can find it in the IAP package assets: Assets -> UnityPurchasing -> scenes -> IAP Demo.

    Code (CSharp):
    1.     /// <summary>
    2.     /// This will be called when Unity IAP has finished initialising.
    3.     /// </summary>
    4.     public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    5.     {
    6.         m_Controller = controller;
    7.         m_AppleExtensions = extensions.GetExtension<IAppleExtensions>();
    8.         m_SamsungExtensions = extensions.GetExtension<ISamsungAppsExtensions>();
    9.         m_MoolahExtensions = extensions.GetExtension<IMoolahExtension>();
    10.         m_MicrosoftExtensions = extensions.GetExtension<IMicrosoftExtensions>();
    11.         m_TransactionHistoryExtensions = extensions.GetExtension<ITransactionHistoryExtensions>();
    12.         m_GooglePlayStoreExtensions = extensions.GetExtension<IGooglePlayStoreExtensions>();
    13.         // Sample code for expose product sku details for google play store
    14.         // Key is product Id (Sku), value is the skuDetails json string
    15.         //Dictionary<string, string> google_play_store_product_SKUDetails_json = m_GooglePlayStoreExtensions.GetProductJSONDictionary();
    16.         // Sample code for manually finish a transaction (consume a product on GooglePlay store)
    17.         //m_GooglePlayStoreExtensions.FinishAdditionalTransaction(productId, transactionId);
    18.         m_GooglePlayStoreExtensions.SetLogLevel(0); // 0 == debug, info, warning, error. 1 == warning, error only.
    19.  
    20.         InitUI(controller.products.all);
    21.  
    22.         // On Apple platforms we need to handle deferred purchases caused by Apple's Ask to Buy feature.
    23.         // On non-Apple platforms this will have no effect; OnDeferred will never be called.
    24.         m_AppleExtensions.RegisterPurchaseDeferredListener(OnDeferred);
    25.  
    26. #if SUBSCRIPTION_MANAGER
    27.         Dictionary<string, string> introductory_info_dict = m_AppleExtensions.GetIntroductoryPriceDictionary();
    28. #endif
    29.         // Sample code for expose product sku details for apple store
    30.         //Dictionary<string, string> product_details = m_AppleExtensions.GetProductDetails();
    31.  
    32.  
    33.         Debug.Log("Available items:");
    34.         foreach (var item in controller.products.all)
    35.         {
    36.             if (item.availableToPurchase)
    37.             {
    38.                 Debug.Log(string.Join(" - ",
    39.                     new[]
    40.                     {
    41.                         item.metadata.localizedTitle,
    42.                         item.metadata.localizedDescription,
    43.                         item.metadata.isoCurrencyCode,
    44.                         item.metadata.localizedPrice.ToString(),
    45.                         item.metadata.localizedPriceString,
    46.                         item.transactionID,
    47.                         item.receipt
    48.                     }));
    49. #if INTERCEPT_PROMOTIONAL_PURCHASES
    50.                 // Set all these products to be visible in the user's App Store according to Apple's Promotional IAP feature
    51.                 // https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/PromotingIn-AppPurchases/PromotingIn-AppPurchases.html
    52.                 m_AppleExtensions.SetStorePromotionVisibility(item, AppleStorePromotionVisibility.Show);
    53. #endif
    54.  
    55. #if SUBSCRIPTION_MANAGER
    56.                 // this is the usage of SubscriptionManager class
    57.                 if (item.receipt != null) {
    58.                     if (item.definition.type == ProductType.Subscription) {
    59.                         if (checkIfProductIsAvailableForSubscriptionManager(item.receipt)) {
    60.                             string intro_json = (introductory_info_dict == null || !introductory_info_dict.ContainsKey(item.definition.storeSpecificId)) ? null : introductory_info_dict[item.definition.storeSpecificId];
    61.                             SubscriptionManager p = new SubscriptionManager(item, intro_json);
    62.                             SubscriptionInfo info = p.getSubscriptionInfo();
    63.                             Debug.Log("product id is: " + info.getProductId());
    64.                             Debug.Log("purchase date is: " + info.getPurchaseDate());
    65.                             Debug.Log("subscription next billing date is: " + info.getExpireDate());
    66.                             Debug.Log("is subscribed? " + info.isSubscribed().ToString());
    67.                             Debug.Log("is expired? " + info.isExpired().ToString());
    68.                             Debug.Log("is cancelled? " + info.isCancelled());
    69.                             Debug.Log("product is in free trial peroid? " + info.isFreeTrial());
    70.                             Debug.Log("product is auto renewing? " + info.isAutoRenewing());
    71.                             Debug.Log("subscription remaining valid time until next billing date is: " + info.getRemainingTime());
    72.                             Debug.Log("is this product in introductory price period? " + info.isIntroductoryPricePeriod());
    73.                             Debug.Log("the product introductory localized price is: " + info.getIntroductoryPrice());
    74.                             Debug.Log("the product introductory price period is: " + info.getIntroductoryPricePeriod());
    75.                             Debug.Log("the number of product introductory price period cycles is: " + info.getIntroductoryPricePeriodCycles());
    76.                         } else {
    77.                             Debug.Log("This product is not available for SubscriptionManager class, only products that are purchase by 1.19+ SDK can use this class.");
    78.                         }
    79.                     } else {
    80.                         Debug.Log("the product is not a subscription product");
    81.                     }
    82.                 } else {
    83.                     Debug.Log("the product should have a valid receipt");
    84.                 }
    85. #endif
    86.             }
    87.         }
    88.  
    89.         // Populate the product menu now that we have Products
    90.         AddProductUIs(m_Controller.products.all);
    91.  
    92.         LogProductDefinitions();
    93.     }
     
  5. pawadski

    pawadski

    Joined:
    Apr 13, 2020
    Posts:
    12
    Thank you. This is helpful. Should HasReceipt be true and product.receipt contain a receipt when using test purchases on Android through Google Play? Currently it is false and receipt is null even though the order is complete at Google Play.
     
  6. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    What product type? Can you confirm with a test subscription?
     
  7. pawadski

    pawadski

    Joined:
    Apr 13, 2020
    Posts:
    12
    I only have one product and it is a non-consumable (not subscription), are these unsupported?

    The receipt is null and HasReceipt is false in OnInitialized, it is not false in the purchase events that fire from button etc.
     
  8. pawadski

    pawadski

    Joined:
    Apr 13, 2020
    Posts:
    12
  9. edwardrowe

    edwardrowe

    Joined:
    Feb 11, 2014
    Posts:
    52
    Dug this up trying to find > the preferred method to deal with ensuring the user still has the purchase active on app start.

    It would be helpful if the docs listed what the INTENDED way to do this is, for non-consumables. All the samples seem to assume we are using a subscription for full app unlock (and maybe we should be using a subscription?)

    I'll post here if I figure it out.
     
  10. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    Do you have access to the product receipt? You would want to check during IAP initialization. Please show your current code. Also, you can look at the Sample IAP Project v2 for an example https://forum.unity.com/threads/sample-iap-project.529555/#post-6950270
     
  11. edwardrowe

    edwardrowe

    Joined:
    Feb 11, 2014
    Posts:
    52
    I haven't verified this works for a revoked purchase yet (can't figure out how to do that on the store page yet), but here's what is working for verifying they have the purchase.

    First, I want to be extra clear about what problem I'm solving, for future readers:
    1. We want an in-app purchase to unlock the full game (Android only - via a non-consumable)
    2. Once they've purchased the IAP, we need to actually unlock the content, which is already downloaded and stored locally.
    3. We need to unlock it in such a way that on subsequent launches, the app is fully unlocked.
    4. We'd like the verification process to be Secure (not hackable) and Not require Internet

    Our solution is to verify the receipt in OnInitialized, which we believe will work offline as the OS should cache the purchase. We do this on the Title screen.


    private void UnlockPreviousPurchase()
    {
    Debug.Log("UnityIAP: Unlocking Any Previous Purchases");
    var product = this.controller.products.WithID(FullGameID);
    if (product.availableToPurchase && product.hasReceipt)
    {
    VerifyAndUnlockPurchaseForReceipt(product.receipt);
    }
    }


    You can see the guts of `VerifyAndUnlockPurchaseForReceipt` in the full code below, but it's largely just the sample code provided by Unity.

    Once verified, we just set a static flag that says it's unlocked. Since it's static, it gets cleared on the next launch, and we re-verify the purchase again. We do not store anything about the purchase, or whether the app is unlocked. We are thinking this will be secure (not hackable).

    The button to purchase the app is on the Title screen, and we hide the button once it's unlocked.

    Full code below, which is mostly taken from the Unity Learn post on IAP:

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Purchasing;
    3. using UnityEngine.Purchasing.Security;
    4.  
    5. /// <summary>
    6. /// This class is primarly taken from Unity's example:
    7. /// https://learn.unity.com/tutorial/unity-iap#5c7f8528edbc2a002053b46e
    8. /// </summary>
    9. public class InAppPurchaser : IStoreListener
    10. {
    11.     public static string FullGameID = "com.redbluegames.sparklite";
    12.  
    13.     private IStoreController controller;
    14.     private IExtensionProvider extensions;
    15.  
    16.     public event System.Action GamePurchased;
    17.  
    18.     public InAppPurchaser()
    19.     {
    20.         var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
    21.         builder.AddProduct(FullGameID, ProductType.NonConsumable);
    22.  
    23.         Debug.Log("UnityIAP: Initializing.");
    24.         UnityPurchasing.Initialize(this, builder);
    25.     }
    26.  
    27.     public void UnlockTheGame()
    28.     {
    29.         BuyProductID(FullGameID);
    30.     }
    31.  
    32.     /// <summary>
    33.     /// Called when Unity IAP is ready to make purchases.
    34.     /// </summary>
    35.     public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    36.     {
    37.         Debug.Log("UnityIAP: Initialized");
    38.         this.controller = controller;
    39.         this.extensions = extensions;
    40.  
    41.         this.UnlockPreviousPurchase();
    42.     }
    43.  
    44.     /// <summary>
    45.     /// Called when Unity IAP encounters an unrecoverable initialization error.
    46.     ///
    47.     /// Note that this will not be called if Internet is unavailable; Unity IAP
    48.     /// will attempt initialization until it becomes available.
    49.     /// </summary>
    50.     public void OnInitializeFailed(InitializationFailureReason error)
    51.     {
    52.         Debug.Log("UnityIAP: Initialize FAILED");
    53.     }
    54.  
    55.     /// <summary>
    56.     /// Called when a purchase completes.
    57.     ///
    58.     /// May be called at any time after OnInitialized().
    59.     /// </summary>
    60.     public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
    61.     {
    62.         VerifyAndUnlockPurchaseForReceipt(e.purchasedProduct.receipt);
    63.  
    64.         Debug.Log(string.Format("UnityIAP: ProcessPurchase: PASS. Product: '{0}'", e.purchasedProduct.definition.id));
    65.         return PurchaseProcessingResult.Complete;
    66.     }
    67.  
    68.     private void UnlockPreviousPurchase()
    69.     {
    70.         Debug.Log("UnityIAP: Unlocking Any Previous Purchases");
    71.         var product = this.controller.products.WithID(FullGameID);
    72.         if (product.availableToPurchase && product.hasReceipt)
    73.         {
    74.             VerifyAndUnlockPurchaseForReceipt(product.receipt);
    75.         }
    76.     }
    77.  
    78.     private bool VerifyAndUnlockPurchaseForReceipt(string receipt)
    79.     {
    80.         bool validPurchase = true;
    81.  
    82. #if UNITY_ANDROID
    83.         Debug.Log("UnityIAP: Validating Receipt: " + receipt);
    84.         // Prepare the validator with the secrets we prepared in the Editor
    85.         // obfuscation window.
    86.         var validator = new CrossPlatformValidator(GooglePlayTangle.Data(),
    87.             AppleTangle.Data(), Application.identifier);
    88.  
    89.         try
    90.         {
    91.             var result = validator.Validate(receipt);
    92.  
    93.             Debug.Log("UnityIAP: Receipt is valid. Contents:");
    94.             foreach (IPurchaseReceipt productReceipt in result)
    95.             {
    96.                 Debug.Log(productReceipt.productID);
    97.                 Debug.Log(productReceipt.purchaseDate);
    98.                 Debug.Log(productReceipt.transactionID);
    99.             }
    100.         }
    101.         catch (IAPSecurityException)
    102.         {
    103.             Debug.Log("UnityIAP: Invalid receipt, not unlocking content");
    104.             validPurchase = false;
    105.  
    106. #if UNITY_EDITOR
    107.             Debug.Log("UnityIAP: Just kidding, we call invalid receipts in Editor valid because Editor throws IAPSecurityException.");
    108.             validPurchase = true;
    109. #endif
    110.         }
    111. #endif
    112.  
    113.         if (validPurchase)
    114.         {
    115.             Debug.Log("UnityIAP: This was a valid purchase. Unlocking content based on purchase.");
    116.             ApplicationManager.Instance.SetFullGameUnlocked(true);
    117.             GamePurchased?.Invoke();
    118.         }
    119.  
    120.         return validPurchase;
    121.     }
    122.  
    123.     /// <summary>
    124.     /// Called when a purchase fails.
    125.     /// </summary>
    126.     public void OnPurchaseFailed(Product i, PurchaseFailureReason p)
    127.     {
    128.         Debug.Log(string.Format("UnityIAP: ProcessPurchase: FAIL. Product: '{0}', Reason: {1}", i.definition.id, p));
    129.     }
    130.  
    131.     private void BuyProductID(string productId)
    132.     {
    133.         if (IsInitialized())
    134.         {
    135.             Product product = controller.products.WithID(productId);
    136.  
    137.             if (product != null && product.availableToPurchase)
    138.             {
    139.                 Debug.Log(string.Format("UnityIAP: Purchasing product asychronously: '{0}'", product.definition.id));
    140.  
    141.                 // Async purchase call
    142.                 controller.InitiatePurchase(product);
    143.             }
    144.             else
    145.             {
    146.                 Debug.Log("UnityIAP: BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
    147.             }
    148.         }
    149.         else
    150.         {
    151.             Debug.Log("UnityIAP: BuyProductID FAIL. Not initialized.");
    152.         }
    153.     }
    154.  
    155.     private bool IsInitialized()
    156.     {
    157.         return controller != null && extensions != null;
    158.     }
    159. }
     
  12. vladimir-fs

    vladimir-fs

    Joined:
    Nov 28, 2019
    Posts:
    22
    We're using the same approach to validation on Android. The issue we're facing with this is that signing out of a Google account and logging into a different one will pass validation and give the second account access to the paid content. I'm not sure how to get around that.
     
  13. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    How are you "signing out of a Google account", it uses the email signed into Google Play on the device.
     
  14. AppSD

    AppSD

    Joined:
    Feb 9, 2021
    Posts:
    11
    @JeffDUnity, the sample IAP project you linked has a strange problem, and I am curious if anyone else has encountered the error. If you exist the the scene the IAP script is attached to and come back later, tapping any of the purchase buttons, the error of "MissingReferenceException: The object type of 'Text' has been destroyed but you are still trying to access it". Sometimes, the console says that the Script itself is destroyed. The strange thing is that Debug.Log function is fine. And there is no problem to tap on restore button to access MyDebug function. Could you point out how this can be solved!
     
  15. SamOYUnity3D

    SamOYUnity3D

    Unity Technologies

    Joined:
    May 12, 2019
    Posts:
    600
    This does not sound like a problem with the Demo. After loading other scenes, the current scene will be destroyed, so you may need to deal with this situation.
     
  16. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    It sounds like you have modified the sample. You can simply comment out the offending line otherwise.
     
  17. frikic

    frikic

    Joined:
    Dec 22, 2011
    Posts:
    44
    Hello, I'm a bit confused and not sure if I got all that's been said here.
    So
    1. There is no way to check if Non-Consumable has be bought by user and then unlock the content when OnInitialized is called?
    2. The only way suggested is to have it saved to PlayerPrefs and then read from there?

    But I have few issues with that
    1. We are required by Google Play Pass to check, if user is still eligible for their subscription every time user starts using the app, they say that u can use getPurchases to get this info. So how do I do that from unity IAP, or do I have to write native code to achieve this?
    2. Plus we need user to be able to use the app offline, and both IOS and Android, so PlayerPrefs could be used in this sense I guess, but is using Local receipt validation more secure and doable as @edwardrowe wrote his example?
     
  18. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    Yes, you check the receipt. It's a property of each of your products in your controller. You can also use the IAP SubscriptionManager. There is an example in the Sample project, and also within the Samples for In App Purchasing library in Package Manager.
     
  19. frikic

    frikic

    Joined:
    Dec 22, 2011
    Posts:
    44
    Thanks for the reply! but not sure if I was clear here, we have NON-Consumable product, that we need to unlock if we find that user is subscribed to Google Play Pass, so subscription I was referring to was Google Play Pass which we cant access directly.
    In their explanation they say that we should use getPurchases for durable(their name for non-consumable) IAP content.
    Is it same procedure as you explained above?
     
  20. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    Does the product have a receipt? Please share the code that you are using to check the receipt.
     
  21. frikic

    frikic

    Joined:
    Dec 22, 2011
    Posts:
    44
    Code (CSharp):
    1. public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    2.     {
    3.         // Purchasing has succeeded initializing. Collect our Purchasing references.
    4.         Debug.Log("OnInitialized: PASS");
    5.  
    6.         // Overall Purchasing system, configured with products for this application.
    7.         m_StoreController = controller;
    8.         // Store specific subsystem, for accessing device-specific store features.
    9.         m_StoreExtensionProvider = extensions;
    10.        
    11.         foreach (var product in controller.products.all)
    12.         {
    13.             if (product.definition.id == kProductIDNonConsumable)
    14.             {
    15.                 if (!product.hasReceipt)
    16.                 {
    17.                     buyButton.interactable = true;
    18.                 }
    19.                 // set the price text for the app upgrade
    20.                 priceText.text = product.metadata.localizedPriceString;
    21.                
    22.                 break;
    23.             }
    24.         }
    25.     }
     
  22. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    So does the product have a receipt? If so, the user has purchased it.
     
  23. frikic

    frikic

    Joined:
    Dec 22, 2011
    Posts:
    44
    Yes sure, but Google Play Pass will revoke this if user cancels their subscription, so we need to check if this happened
    each time we start the game, so from reading previous post I got idea that this is not going to be reflected here..
     
  24. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    If the product is revoked, there should not be a receipt.
     
  25. frikic

    frikic

    Joined:
    Dec 22, 2011
    Posts:
    44
    Ok thanks, so its simple solution, just a note that I was referring to this lines

    "I only have one product and it is a non-consumable (not subscription), are these unsupported?
    The receipt is null and HasReceipt is false in OnInitialized, it is not false in the purchase events that fire from button etc."

    and your answer here

    "Yes that is expected behavior for non-consumables, as mentioned. You can store purchase information in PlayerPrefs, but that has its limitations and may be deleted during app reinstall."
     
  26. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    Apologies, I misunderstood their question.
     
    frikic likes this.
  27. frikic

    frikic

    Joined:
    Dec 22, 2011
    Posts:
    44
    No problem and thanks for your help!
     
  28. IndieFist

    IndieFist

    Joined:
    Jul 18, 2013
    Posts:
    515
    Sorry for bump this old thread but i get this error in console
    (54,16): error CS1520: Method must have a return type
     

    Attached Files:

  29. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    Separate topic, I will close this thread. Please open a new thread. But that's your code, not IAP.
     
Thread Status:
Not open for further replies.