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. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice

Question Problem with Apple RefreshAppReceipt and SubscriptionManager and handling free trial eligibility

Discussion in 'Unity IAP' started by Papatriz, Jan 7, 2022.

  1. Papatriz

    Papatriz

    Joined:
    Jan 7, 2014
    Posts:
    14
    Hello!

    While testing the IAP on the device, I ran into the following problem (perhaps this case occurs exclusively in the sandbox, since the receipt is not stored there locally, as I know):
    In the case, if a user launches an application without network access and then connects to the network, the following happens:
    Unity IAP tries to initiate, when it succeeds, it receives the product, but does not receive the receipt for some reason.
    As a result, my PaymentManager decides that this is a new user and marks him as eligible for free trial and does not give him full access even if his subscription is actually active.
    I tried to solve this problem by using m_AppleExtensions.RefreshAppReceipt in case the receipt is empty.
    As a result, I get a receipt as a string successfully, but the product.receipt remains empty.

    To solve it I tried to create a SubscriptionManager instance using a constructor with a signature (string receipt, string id, string intro_json). It was successful, but when I call the getSubscriptionInfo() method, I get a Null Reference exception. After investigation I realised that it highly likely because SubManager ctor use Unity IAP unified receipt and m_AppleExtensions.RefreshAppReceipt returns Apple specific receipt. But I still confused, why RefreshAppReceipt does not push receipt to product.receipt and what I can to do in this situation.
    This is probably a sandbox problem only ( but I am not sure :( ), but I would like to sort it out, since I planned to use a similar method to determine the availability of a free trial for users who have reinstalled the application.

    Also, I would be very grateful if someone could suggest a good way to determine the eligibility of a free trial for users who reinstalled the application. Basically I have a very simple scenario - I only use one subscription with a free trial. And in this case, if there is a receipt, then the user has used a free trial, if not, then no.
    But in the case of reinstallation, problems arise, as I understand it. And also I don't want to use refresh receipt if there is better way, cause it may prompt for user credential (in sandbox it didn't but I read it may).

    I know that many people use their servers or third-party services for these purposes, but I highly discourage using my server for receipt validation or data storage, since I develop children's applications and any external connections are a headache due to very strict store policies (and also I don't care about validation, cause fraud doesn't problem for me due to specificity of my target group).
    Previously, I only used one-time payments, so the subscription story is new to me, and I would be grateful for any help.

    PS Below some code to illustrate:
    Code (CSharp):
    1.            
    2. if (subscriptionProduct.receipt == null && string.IsNullOrEmpty(RefreshedReceipt)) // There were no purchases in the past
    3.             {
    4.                 SubsData.FreeTrialAvailable = true;
    5.                 SubsData.SubscriptionIsActive = false;
    6.  
    7.                 SubscriptionStatusUpdated();
    8.                 return;
    9.             }
    10.  
    11. var subscriptionManager = subscriptionProduct.receipt != null ? new SubscriptionManager(subscriptionProduct, null) : new SubscriptionManager(RefreshedReceipt, subscriptionProductId, null);
    12.  
    13.             try
    14.             {
    15.                 var info = subscriptionManager.getSubscriptionInfo();
    16.  
    17.                 SubsData.FreeTrialAvailable = false;
    18.                 SubsData.SubscriptionIsActive = info.isSubscribed() == Result.True;
    19.                 SubsData.ExpirationDate = info.getExpireDate();
    20.             }
    21.             catch (Exception ex)
    22.             {
    23.                 Debug.Log("SyncSubStatus: catch exception in " + ex.Source + " \nMessage:" + ex.Message + "\n"+ex.StackTrace);
    24.             }
     
  2. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    @Papatriz You mention the following "Unity IAP tries to initiate, when it succeeds, it receives the product, but does not receive the receipt for some reason." What product and what receipt are you referring to? How are you debugging? Please provide the device logs and ensure to place descriptive Debug.Log messages throughout your code. And you are asking several questions, first about network connectivity, then about free trials. I would recommend to take it one step at a time. First, test with full network connectivity and get everything working. Regarding free trials, we don't currently have a good solution to your problem. There is an "isFreeTrial" entry in the receipt, but it only specifies if the product itself is eligible for a free trial, not whether the user is entitled to one, is in a free trial, or has had a free trial in the past. We are hoping that Google Billing Library v4 (and above) will improve this behavior. We are a pass-through service for the store APIs so we depend on them for this functionality. Otherwise, you will need to persist this information yourself. You can prototype with PlayerPrefs, but eventually will want to consider cloud save.
     
  3. Papatriz

    Papatriz

    Joined:
    Jan 7, 2014
    Posts:
    14
    Thank you for your response. With full network connectivity everything works fine.
    I've test scenario where user run app without internet and then turn it on without app restart.
    Full log from device I'll attach as text file.

    As a product I mean product which we added here:
    builder.AddProduct(subscriptionProductId, ProductType.Subscription).
    In my case it is only one subscription

    Below some code with my additional comments for you:
    Code (CSharp):
    1. public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    2. {
    3.     m_StoreController = controller;
    4.     m_AppleExtensions = extensions.GetExtension<IAppleExtensions>();
    5.  
    6.     IsInitialized = true;
    7.     Debug.Log("In-App Purchasing successfully initialized");
    8.  
    9.     Dictionary<string, string> introductory_info_dict = m_AppleExtensions.GetIntroductoryPriceDictionary();
    10.  
    11.     Debug.Log("Available items:");
    12.     foreach (var item in controller.products.all)
    13.     {
    14.         if (item.availableToPurchase)
    15.         {
    16.             Debug.Log(string.Join(" - ",
    17.                 new[]
    18.                 {
    19.             item.metadata.localizedTitle,
    20.             item.metadata.localizedDescription,
    21.             item.metadata.isoCurrencyCode,
    22.             item.metadata.localizedPrice.ToString(),
    23.             item.metadata.localizedPriceString,
    24.             item.transactionID,
    25.             item.receipt
    26.                 }));
    27.  
    28.             if (item.receipt != null) // Everything is OK
    29.             {
    30.                 if (item.definition.type == ProductType.Subscription)
    31.                 {
    32.                     SyncSubStatus();
    33.                     SubscriptionStatusUpdated();
    34.                 }
    35.                 else
    36.                 {
    37.                     Debug.Log("the product is not a subscription product");
    38.                 }
    39.             }
    40.             else // We have no receipt for this product
    41.             {
    42.                 Debug.Log("the product should have a valid receipt");
    43.                 Refresh();  // Here I call RefreshReceipt
    44.             }
    45.         }
    46.     }
    47. }
    48.  
    49.         void Refresh()
    50.         {
    51.             m_AppleExtensions.RefreshAppReceipt(OnRefreshSuccess, OnRefreshFailure);
    52.         }
    53.  
    54.         void OnRefreshSuccess(string receipt)
    55.         {
    56.             RefreshedReceipt = receipt;
    57.             Debug.Log("Refresh Successful, receipt is " + receipt);
    58.  
    59.             SyncSubStatus();
    60.             SubscriptionStatusUpdated();
    61.         }
    62.  
    63.         void OnRefreshFailure()
    64.         {
    65.             Debug.Log("Refresh Failed");
    66.  
    67.             SyncSubStatus();
    68.             SubscriptionStatusUpdated();
    69.         }
    70.  
    71. void SyncSubStatus()
    72.         {
    73.             var SubsData = CompositionRoot.GetSubsData();
    74.  
    75.             var subscriptionProduct = m_StoreController.products.WithID(subscriptionProductId);
    76.  
    77.             var localPrice = subscriptionProduct.metadata.localizedPriceString;
    78.  
    79.             SubsData.LocalizedPrice = localPrice;
    80.  
    81.             if (subscriptionProduct.receipt == null && string.IsNullOrEmpty(RefreshedReceipt)) // There were no purchases in the past
    82.             {
    83.                 SubsData.FreeTrialAvailable = true;
    84.                 SubsData.SubscriptionIsActive = false;
    85.                 BinarySaveSystem.Save(SubsData);
    86.  
    87.                 Debug.Log("SybcSubSTatus: no receipt");
    88.                 SubscriptionStatusUpdated();
    89.  
    90.                 return;
    91.             }
    92.  
    93.             var subscriptionManager = subscriptionProduct.receipt != null ? new SubscriptionManager(subscriptionProduct, null) : new SubscriptionManager(RefreshedReceipt, subscriptionProductId, null);
    94.  
    95.             Debug.Log("SyncSubStatus: SubManager is null: " + (subscriptionManager is null));
    96.  
    97.             try
    98.             {
    99.                 var info = subscriptionManager.getSubscriptionInfo();
    100.  
    101.                 Debug.Log("SyncSubStatus: IsSubscribed=" + info.isSubscribed() + "  Expiration date: " + info.getExpireDate());
    102.  
    103.                 SubsData.FreeTrialAvailable = false;
    104.                 SubsData.SubscriptionIsActive = info.isSubscribed() == Result.True;
    105.                 SubsData.ExpirationDate = info.getExpireDate();
    106.                 BinarySaveSystem.Save(SubsData);
    107.             }
    108.             catch (Exception ex)
    109.             {
    110.                 Debug.Log("SyncSubStatus: catch exception in " + ex.Source + " \nMessage:" + ex.Message + "\n"+ex.StackTrace);
    111.             }
    112.  
    113.         }
    I attach result of this as device log from Xcode.
    In a nutshell:
    Start app without network connection.
    Unity IAP try to initialise, but failed.
    Then device connected to network.
    Unity IAP initialised successfully and get product "Monthly subscription", but without receipt.
    So Refresh method started and get actual receipt (I cut it in log, cause it to big after 12 cycles of renew subscription in the Sandbox). But subscriptionProduct.receipt is still null, RefreshReceipt doesn't set it.
    Then SubscriptionManager created in SyncSub method:
    .... new SubscriptionManager(RefreshedReceipt, subscriptionProductId, null);
    And then I tried to get subscriptionManager.getSubscriptionInfo() I catch exception.

    Thank you again for your help!
     

    Attached Files:

  4. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    @Papatriz There won't be a product receipt until the user purchases. The app receipt is not an IAP product receipt. Sorry I would not be able to debug your code. Please use the Sample IAP Project here (with no changes) and see if you see the same behavior. You have a null exception in your code that you will need to fix. You are not checking for a null receipt would be my guess. https://forum.unity.com/threads/sample-iap-project.529555/#post-6950270

    I don't follow your steps:

    Unity IAP try to initialise, but failed. <- Do you receive the InitializedFailed callback? We don't keep trying to connect after that
    Then device connected to network.
    Unity IAP initialised successfully <- Do you attempt to initialize again?
     
  5. Papatriz

    Papatriz

    Joined:
    Jan 7, 2014
    Posts:
    14
    User has active subscription in this test case.

    No, I didn't receive InitializedFailed callback (it would be better cause I handle this scenario, but it doesn't happen)
    Instead I've got repeating message in device log (it's Unity IAP self behaviour, not my code!):

    2022-01-07 21:23:27.201617+0300 Smarty[27166:2460696] UnityIAP: Requesting product data...

    2022-01-07 21:23:27.239360+0300 Smarty[27166:2460927] UnityIAP: SKProductRequest::didFailWithError: 0, Error Domain=SKErrorDomain Code=0 "Произошла неизвестная ошибка" UserInfo={NSLocalizedDescription=Произошла неизвестная ошибка, NSUnderlyingError=0x2832e49c0 {Error Domain=ASDErrorDomain Code=500 "Unhandled exception" UserInfo={NSUnderlyingError=0x2832e5350 {Error Domain=AMSErrorDomain Code=203 "Bag Load Failed" UserInfo={NSLocalizedFailureReason=Unable to retrieve p2-product-offers-batch-limit because we failed to load the bag., NSLocalizedDescription=Bag Load Failed, NSUnderlyingError=0x2832e5650 {Error Domain=AMSErrorDomain Code=203 "Bag Load Failed" UserInfo=0x282802d80 (not displayed)}}}, NSLocalizedFailureReason=An unknown error occurred, NSLocalizedDescription=Unhandled exception}}}.

    Unity Purchasing will retry in 32 seconds
    (Cyrillic text means "Unknown error happens")

    This behaviour continues until network connection established.

    After that, the following happens:

    2022-01-07 21:25:00.648763+0300 Smarty[27166:2460696] UnityIAP: Requesting product data...
    2022-01-07 21:25:02.975053+0300 Smarty[27166:2462018] UnityIAP: Received 1 products

    In-App Purchasing successfully initialized
    UnityEngine.DebugLogHandler:Internal_Log(LogType, LogOption, String, Object)
    UnityEngine.DebugLogHandler:LogFormat(LogType, Object, String, Object[])
    UnityEngine.Logger:Log(LogType, Object)
    UnityEngine.Debug:Log(Object)
    KidGame.Service.PaymentManager:OnInitialized(IStoreController, IExtensionProvider)
    UnityEngine.Purchasing.StoreListenerProxy:OnInitialized(IStoreController)
    UnityEngine.Purchasing.PurchasingManager:CheckForInitialization()
    UnityEngine.Purchasing.PurchasingManager:OnProductsRetrieved(List`1)
    UnityEngine.Purchasing.AppleStoreImpl:OnProductsRetrieved(String)
    UnityEngine.Purchasing.AppleStoreImpl:processMessage(String, String, String, String)
    UnityEngine.Purchasing.<>c__DisplayClass47_0:<MessageCallback>b__0()
    UnityEngine.Purchasing.Extension.UnityUtil:Update()


    Full access (monthly) - Full access on a monthly basis - RUB - 379 - 379,00 ₽ - -

    UnityEngine.DebugLogHandler:Internal_Log(LogType, LogOption, String, Object)
    UnityEngine.DebugLogHandler:LogFormat(LogType, Object, String, Object[])
    UnityEngine.Logger:Log(LogType, Object)
    UnityEngine.Debug:Log(Object)
    KidGame.Service.PaymentManager:OnInitialized(IStoreController, IExtensionProvider)
    UnityEngine.Purchasing.StoreListenerProxy:OnInitialized(IStoreController)
    UnityEngine.Purchasing.PurchasingManager:CheckForInitialization()
    UnityEngine.Purchasing.PurchasingManager:OnProductsRetrieved(List`1)
    UnityEngine.Purchasing.AppleStoreImpl:OnProductsRetrieved(String)
    UnityEngine.Purchasing.AppleStoreImpl:processMessage(String, String, String, String)
    UnityEngine.Purchasing.<>c__DisplayClass47_0:<MessageCallback>b__0()
    UnityEngine.Purchasing.Extension.UnityUtil:Update()


    the product should have a valid receipt. <=== This Debug output means product.receipt is null

    UnityEngine.DebugLogHandler:Internal_Log(LogType, LogOption, String, Object)
    UnityEngine.DebugLogHandler:LogFormat(LogType, Object, String, Object[])
    UnityEngine.Logger:Log(LogType, Object)
    UnityEngine.Debug:Log(Object)
    KidGame.Service.PaymentManager:OnInitialized(IStoreController, IExtensionProvider)
    UnityEngine.Purchasing.StoreListenerProxy:OnInitialized(IStoreController)
    UnityEngine.Purchasing.PurchasingManager:CheckForInitialization()
    UnityEngine.Purchasing.PurchasingManager:OnProductsRetrieved(List`1)
    UnityEngine.Purchasing.AppleStoreImpl:OnProductsRetrieved(String)
    UnityEngine.Purchasing.AppleStoreImpl:processMessage(String, String, String, String)
    UnityEngine.Purchasing.<>c__DisplayClass47_0:<MessageCallback>b__0()
    UnityEngine.Purchasing.Extension.UnityUtil:Update()


    If I run app with Internet connection from the start, all things are ok. Including getting receipt.

    Next part actually doesn't matter due to I realise, that receipt from m_AppleExtensions.RefreshAppReceipt is not suitable for SubscriptionManager and highly likely this causes exception.
     
  6. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    @Papatriz I don't see OnInitializeFailed in your code above. But I do see an issue. For a user that hasn't made a purchase yet, this callback is called if there is no network. However, initialization succeeds if the user has made a purchase previously and there is no network, I just tested. But then all subsequent purchases return Unknown as the failure reason. Be sure to be using IAP 4.1.2 (latest as of today). At any rate, I will let engineering know. I believe the correct approach would be to call OnInitializeFailed if there is no network regardless.
     
  7. Papatriz

    Papatriz

    Joined:
    Jan 7, 2014
    Posts:
    14
    Of course I have OnInitializeFailed in my code, just didn't post it in topic. And yes, I use 4.1.2 IAP.
    BTW, thank you very much for your support. Actually current behaviour isn't bad, the only issue is empty product.receipt after reconnection, but I guess this happens due to Apple Sandbox environment features, will test it on Android soon.
    But getting callback InitializedFailed would be nice, even if IAP will continue trying to initialise in background.
    At least we will have more information and will be able to implement more flexible scenarios.
    Thanks again!
     
  8. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    Please post full code in the future to avoid confusion. Once addressed, IAP would not continue to try to connect once OnInitializeFailed is called.