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

UnityIAP purchase receipt json is malformatted

Discussion in 'Editor & General Support' started by WendelVolm, May 24, 2016.

?

Is this a Unity bug?

  1. Yes

    71.4%
  2. No

    28.6%
  1. WendelVolm

    WendelVolm

    Joined:
    May 24, 2016
    Posts:
    1
    Hi all,

    My project was setup according to the UnityIAP guide. Everything works perfectly, including Google Play purchases after uploading the APK to the developer's console.

    Unfortunately, I run into a problem parsing the JSON from the "payload" field of the purchase receipt (which I need for server side verification).

    In the ProcessPurchase function of IStoreListener, when a purchase completes successfully, the payload value of the receipt (e.purchasedProduct.receipt) appears to have incorrect JSON. This is both when I buy something in the editor to test, and when I make an actual purchase when running on Android.

    When I log the receipt of the editor, it looks like this:
    {"Store":"fake","TransactionID":"aaa995f8-496a-4781-91f9-41bfdb9a6b6a","Payload":"{ \"this\" : \"is a fake receipt\" }"}

    The Google Play receipt looks like this:
    {"Store":"GooglePlay","TransactionID":"GPA.1357-7815-3377-93231","Payload":"{\"json\":\"{\\\"orderId\\\":\\\"GPA.1357-7815-3377-93231\\\",\\\"packageName\\\":\\\"com.package.testgame\\\",\\\"productId\\\":\\\"100pts\\\",\\\"purchaseTime\\\":1464037549905,\\\"purchaseState\\\":0,\\\"purchaseToken\\\":\\\"koihbeblaqcpblo.AP-K1Oz--Grhs2FBiwsmKriwu8oYMPLjXrBzBbOs0FiLK4KeUeZ\\\"}\",\"signature\":\"fy45IekU73QBeeDpEFL05qbS\\/TSWA9io23HxfVyaCfOziXe7+5JfxrKSW1H43jTOf8BYw3E7Q==\"}"}

    The JSON in the payload has incorrect backslashes (I marked some in red). Which could be the result of escaping the double quotes somewhere in the Unity code. On Android, the purchase gets logged also before it is passed to the ProcessPurchase function, at which time the whole JSON is perfectly fine.

    I would like to solve this problem to be able to parse the payload to do server side verification. I already tried the latest beta, but that gave the same result. Also started a whole new project with only the IAP code. The same happens in the IAP Demo scene. I could manually remove the backslashes, but I would much rather find a real fix. If there is no such thing possible on my end, should i send in a bug report for this?

    Thanks a lot for the help.
     
  2. Banderous

    Banderous

    Joined:
    Dec 25, 2011
    Posts:
    669
    This is as expected. Payload is a string, which in this case is JSON which must be escaped with backslashes. On google play this itself contains json, so is doubly escaped.

    You need to parse the whole receipt string as json, then the payload property, then the fields of the payload. The structure is documented here.
     
    nicholasr likes this.
  3. nicholasr

    nicholasr

    Unity Technologies

    Joined:
    Aug 15, 2015
    Posts:
    183
  4. unityuserBOB

    unityuserBOB

    Joined:
    Sep 28, 2016
    Posts:
    3
  5. unityuserBOB

    unityuserBOB

    Joined:
    Sep 28, 2016
    Posts:
    3
    Arrrrr: Solved
    The json string included \ for escaping "
    and also for function call m_StoreController.InitiatePurchase() the developer_payload random string optionally passed.
     
  6. ignitiongames

    ignitiongames

    Joined:
    Feb 27, 2015
    Posts:
    20
    I think that this is a bug. The json that comes in is not valid with any parsers and this is not the case with the returned data from an App Store purchase. I think that this should be consistent across all store platforms.
     
  7. SplashFoxGames

    SplashFoxGames

    Joined:
    Oct 10, 2012
    Posts:
    53
    any solution so far? what if just to replace any \\\ or \\ or \ to string.empty ? it will not break purchase token etc?
     
  8. piyushpandeyDrpanda

    piyushpandeyDrpanda

    Joined:
    Dec 17, 2018
    Posts:
    9
    Hi @nicholasr

    We are facing some issues with Google receipt. When we get the receipt, we try to save it for the offline validation in PlayerPrefs so that if the user if offline then we can use it to verify the user to unlock content. But the problem is we are not getting the correct expiry date from the SubscriptionInfo class. The expiry date is the same as the purchase date. The code we used is:--

    Code (CSharp):
    1. string introJson =
    2.     (IntroductoryInfo == null ||
    3.      !IntroductoryInfo.ContainsKey(_purchaseEventArgsCache.purchasedProduct.definition.storeSpecificId))
    4.         ? null
    5.         : IntroductoryInfo[_purchaseEventArgsCache.purchasedProduct.definition.storeSpecificId];
    6.  
    7. DPLogger.Log("intro JSON is : " + introJson);
    8.  
    9. SubscriptionManager subscriptionManager =
    10.     new SubscriptionManager(_purchaseEventArgsCache.purchasedProduct, introJson);
    11.  
    12. DPLogger.Log("SubscriptionManager is : " + subscriptionManager);
    13.  
    14. SubscriptionInfo subscriptionInfo = subscriptionManager.getSubscriptionInfo();
    15.  
    16. DPLogger.Log("SubscriptionInfo is : " + subscriptionInfo);
    17.  
    18. //--------
    19.  
    20.  
    21. DPLogger.Log("receipt is : " + _purchaseEventArgsCache.purchasedProduct.receipt);
    22.  
    23. DPLogger.Log("getProductId is : " + subscriptionInfo.getProductId());
    24.  
    25. DPLogger.Log("getPurchaseDate is : " + subscriptionInfo.getPurchaseDate());
    26.  
    27. DPLogger.Log("isSubscribed is : " + subscriptionInfo.isSubscribed());
    28.  
    29. DPLogger.Log("isExpired is : " + subscriptionInfo.isExpired());
    30.  
    31. DPLogger.Log("isCancelled is : " + subscriptionInfo.isCancelled());
    32.  
    33. DPLogger.Log("isFreeTrial is : " + subscriptionInfo.isFreeTrial());
    34.  
    35. DPLogger.Log("isAutoRenewing is : " + subscriptionInfo.isAutoRenewing());
    36.  
    37. DPLogger.Log("getRemainingTime is : " + subscriptionInfo.getRemainingTime());
    38.  
    39. DPLogger.Log("isIntroductoryPricePeriod is : " + subscriptionInfo.isIntroductoryPricePeriod());
    40.  
    41. DPLogger.Log("getIntroductoryPricePeriod is : " + subscriptionInfo.getIntroductoryPricePeriod());
    42.  
    43. DPLogger.Log("getIntroductoryPrice is : " + subscriptionInfo.getIntroductoryPrice());
    44.  
    45. DPLogger.Log("getIntroductoryPricePeriodCycles is : " +
    46.              subscriptionInfo.getIntroductoryPricePeriodCycles());
    47.  
    48. DPLogger.Log("getExpireDate is : " + subscriptionInfo.getExpireDate());
    49.  
    50. DPLogger.Log("getCancelDate is : " + subscriptionInfo.getCancelDate());
    51.  
    52. DPLogger.Log("getFreeTrialPeriod is : " + subscriptionInfo.getFreeTrialPeriod());
    53.  
    54. DPLogger.Log("getSubscriptionPeriod is : " + subscriptionInfo.getSubscriptionPeriod());
    55.  
    56. DPLogger.Log("getFreeTrialPeriodString is : " + subscriptionInfo.getFreeTrialPeriodString());
    57.  
    58. DPLogger.Log("getSkuDetails is : " + subscriptionInfo.getSkuDetails());
    59.  
    60. DPLogger.Log("getSubscriptionInfoJsonString is : " + subscriptionInfo.getSubscriptionInfoJsonString());
    61.  
    62. //——————
    63.  

    the logs that we get from Androind studio is:--


    Code (CSharp):
    1. 2019-08-02 18:02:35.613 6020-6118/? I/Unity: getProductId is : com.blahblah
    2.    
    3.     (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
    4. 2019-08-02 18:02:35.613 6020-6118/? I/Unity: getPurchaseDate is : 08/02/2019 10:03:29
    5.    
    6.     (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
    7. 2019-08-02 18:02:35.614 6020-6118/? I/Unity: isSubscribed is : True
    8.    
    9.     (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
    10. 2019-08-02 18:02:35.614 6020-6118/? I/Unity: isExpired is : False
    11.    
    12.     (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
    13. 2019-08-02 18:02:35.614 6020-6118/? I/Unity: isCancelled is : False
    14.    
    15.     (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
    16. 2019-08-02 18:02:35.614 6020-6118/? I/Unity: isFreeTrial is : False
    17.    
    18.     (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
    19. 2019-08-02 18:02:35.614 6020-6118/? I/Unity: isAutoRenewing is : True
    20.    
    21.     (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
    22. 2019-08-02 18:02:35.614 6020-6118/? I/Unity: getRemainingTime is : 00:00:53.9180000
    23.    
    24.     (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
    25. 2019-08-02 18:02:35.614 6020-6118/? I/Unity: isIntroductoryPricePeriod is : False
    26.    
    27.     (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
    28. 2019-08-02 18:02:35.614 6020-6118/? I/Unity: getIntroductoryPricePeriod is : 00:00:00
    29.    
    30.     (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
    31. 2019-08-02 18:02:35.614 6020-6118/? I/Unity: getIntroductoryPrice is : not available
    32.    
    33.     (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
    34. 2019-08-02 18:02:35.614 6020-6118/? I/Unity: getIntroductoryPricePeriodCycles is : 0
    35.    
    36.     (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
    37. 2019-08-02 18:02:35.614 6020-6118/? I/Unity: getExpireDate is : 08/02/2019 10:03:29
    38.    
    39.     (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
    40. 2019-08-02 18:02:35.615 6020-6118/? I/Unity: getCancelDate is : 01/01/0001 00:00:00
    41.    
    42.     (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
    43. 2019-08-02 18:02:35.615 6020-6118/? I/Unity: getFreeTrialPeriod is : 00:00:00
    44.    
    45.     (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
    46. 2019-08-02 18:02:35.615 6020-6118/? I/Unity: getSubscriptionPeriod is : 31.00:00:00
    47.    
    48.     (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
    49. 2019-08-02 18:02:35.615 6020-6118/? I/Unity: getFreeTrialPeriodString is :
    50.  
    51.  

    We tried to use GoogleExtensions.RestoreTransactions() before saving the receipt, but we got the same result. We want to save the receipt because we use our own backend to verify the receipt after a successful purchase and if the next time the user is offline then use that receipt with CrossPlatformValidator() to validate it locally for security. Once we kill the app and start again, the receipt from google is correct.
     
    Last edited: Aug 2, 2019
  9. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    @piyushpandeyDrpanda I would not recommend to use PlayerPrefs to store receipts. PlayerPrefs is often deleted when the user upgrades the app, and is not secure. Also, I would not recommend doing any date math yourself with Subscriptions. If you are testing in Google Alpha/Beta/Internal, the expire date is indeed the same day. A subscription in their test environment only lasts a few minutes. May I ask, why would you want to read the receipt if they are offline to unlock content? If you're going to do that, just set "ProductABC purchase = true" in the PlayerPrefs instead of storing the receipt, and save a few steps. By nature, the receipt was already checked on initial purchase. We do not currently offer a local inventory management component for IAP, but is something we are looking into.
     
  10. piyushpandeyDrpanda

    piyushpandeyDrpanda

    Joined:
    Dec 17, 2018
    Posts:
    9
    #1 "I would not recommend to use PlayerPrefs to store receipts. PlayerPrefs is often deleted when the user upgrades the app, and is not secure." :-
    Yes. True. But there is no other way to check a receipt and unlock content on Google. The same is not the case for iOS. It would be super cool from Unity's side if we can do something like this in google too.

    Code (CSharp):
    1. #if UNITY_ANDROID || UNITY_IOS || UNITY_STANDALONE_OSX
    2. var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
    3. // Get a reference to IAppleConfiguration during IAP initialization.
    4. var appleConfig = builder.Configure<IAppleConfiguration>();
    5. var receiptData = System.Convert.FromBase64String(appleConfig.appReceipt);
    6. AppleReceipt receipt = new AppleValidator(AppleTangle.Data()).Validate(receiptData);
    7.  
    8. Debug.Log(receipt.bundleID);
    9. Debug.Log(receipt.receiptCreationDate);
    10. foreach (AppleInAppPurchaseReceipt productReceipt in receipt.inAppPurchaseReceipts) {
    11.     Debug.Log(productReceipt.transactionIdentifier);
    12.     Debug.Log(productReceipt.productIdentifier);
    13. }
    14. #endif
    This works offline on iOS platform. For google we have:
    Code (CSharp):
    1. var result = validator.Validate(e.purchasedProduct.receipt);
    2. Debug.Log("Receipt is valid. Contents:");
    3. foreach (IPurchaseReceipt productReceipt in result) {
    4.     Debug.Log(productReceipt.productID);
    5.     Debug.Log(productReceipt.purchaseDate);
    6.     Debug.Log(productReceipt.transactionID);
    7.  
    8.     GooglePlayReceipt google = productReceipt as GooglePlayReceipt;
    9.     if (null != google) {
    10.         // This is Google's Order ID.
    11.         // Note that it is null when testing in the sandbox
    12.         // because Google's sandbox does not provide Order IDs.
    13.         Debug.Log(google.transactionID);
    14.         Debug.Log(google.purchaseState);
    15.         Debug.Log(google.purchaseToken);
    16.     }
    Here we will need receipt for Google for the validator to work. Else it cannot. There is no other way to get the receipt except to store it in PlayerPrefs for offline mode.


    #2 "Also, I would not recommend doing any date math yourself with Subscriptions":
    We are using the
    getExpiry()
    method from the
    SubscriptionInfo
    class for the live environment. The real problem is for testing in QA for local receipt validation. We want to keep the content unlocked even if the player purchased subscription but is offline unitll the expiry date.

    #3 "If you are testing in Google Alpha/Beta/Internal, the expire date is indeed the same day":
    That is not true. If we kill the app and start again, the receipt that we get is a correct one. It shows the expiry as next month (suppose we bought monthly subcsription) and not 5 minutes later.

    #4 "A subscription in their test environment only lasts a few minutes":
    That happens only on the google play store app. We can see the expiry/renewal time in the app. But the receipt never expires/renews because the expiry is after 1 month (for monthly subscription...and 1 year for annual subscripton). For internal testing Google should give us expiry after 5 minutes like they say in their documentation, but unfortunately we are not getting that from Unity IAP.

    #5 "May I ask, why would you want to read the receipt if they are offline to unlock content? If you're going to do that, just set "ProductABC purchase = true" in the PlayerPrefs instead of storing the receipt,":
    No we cannot do that because we want the local validation to stop working after the expiry date. If we set it as a bool, then we dont know when the expiry date is (if the user remained offline for a long period of time)
     
    Last edited: Aug 8, 2019
  11. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    Again, no need to store the entire receipt. Just store the expire date. I'm not clear why you would do local validation again if you have validated it already. And you are coding directly for the exception rather than the rule. You should instead code for the rule, and handle the exception. Again, your mileage may vary.
     
  12. piyushpandeyDrpanda

    piyushpandeyDrpanda

    Joined:
    Dec 17, 2018
    Posts:
    9
    The reason for this is it is not secure. On Android the user might tamper the receipt locally. That is the reason we:
    1) Validate the receipt from
    var result = validator.Validate(googleReceipt);
    .
    If the receipt validation failed locally then it means the user tampered with the receipt and the contents will not unlock.

    2) check the expiry date of the receipt once we know the receipt is a valid receipt
     
  13. JeffDUnity3D

    JeffDUnity3D

    Unity Technologies

    Joined:
    May 2, 2017
    Posts:
    14,446
    Exactly! But you are storing the receipt, and you just said yourself that you don't want to store the receipt locally, the user might tamper with it. Just store the date. You should never store the receipt on the device, they could use re-use it for multiple fraudulent purchases.