UnityIAP purchase receipt json is malformatted

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.

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.

1 Like

@WendelVolm And see Getting Unity IAP receipt data to validate with playfab (or somewhere else) - Questions & Answers - Unity Discussions for example code, using MiniJson, extracting the values.

i am validating IAP receipt from our server side, i am extracting the fields using miniJson, when i looked into the receipt fields for android __https://developer.android.com/google/play/billing/billing_reference.html__ the developer_payload field is missing (Similar like above example) in the payload → json. And from server side its always responds validation failed…
Ref:https://docs.unity3d.com/Manual/UnityIAPPurchaseReceipts.html

Arrrrr: Solved
The json string included \ for escaping "
and also for function call m_StoreController.InitiatePurchase() the developer_payload random string optionally passed.

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.

any solution so far? what if just to replace any \\ or \ or \ to string.empty ? it will not break purchase token etc?

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:–

string introJson =
    (IntroductoryInfo == null ||
     !IntroductoryInfo.ContainsKey(_purchaseEventArgsCache.purchasedProduct.definition.storeSpecificId))
        ? null
        : IntroductoryInfo[_purchaseEventArgsCache.purchasedProduct.definition.storeSpecificId];

DPLogger.Log("intro JSON is : " + introJson);

SubscriptionManager subscriptionManager =
    new SubscriptionManager(_purchaseEventArgsCache.purchasedProduct, introJson);

DPLogger.Log("SubscriptionManager is : " + subscriptionManager);

SubscriptionInfo subscriptionInfo = subscriptionManager.getSubscriptionInfo();

DPLogger.Log("SubscriptionInfo is : " + subscriptionInfo);

//--------


DPLogger.Log("receipt is : " + _purchaseEventArgsCache.purchasedProduct.receipt);

DPLogger.Log("getProductId is : " + subscriptionInfo.getProductId());

DPLogger.Log("getPurchaseDate is : " + subscriptionInfo.getPurchaseDate());

DPLogger.Log("isSubscribed is : " + subscriptionInfo.isSubscribed());

DPLogger.Log("isExpired is : " + subscriptionInfo.isExpired());

DPLogger.Log("isCancelled is : " + subscriptionInfo.isCancelled());

DPLogger.Log("isFreeTrial is : " + subscriptionInfo.isFreeTrial());

DPLogger.Log("isAutoRenewing is : " + subscriptionInfo.isAutoRenewing());

DPLogger.Log("getRemainingTime is : " + subscriptionInfo.getRemainingTime());

DPLogger.Log("isIntroductoryPricePeriod is : " + subscriptionInfo.isIntroductoryPricePeriod());

DPLogger.Log("getIntroductoryPricePeriod is : " + subscriptionInfo.getIntroductoryPricePeriod());

DPLogger.Log("getIntroductoryPrice is : " + subscriptionInfo.getIntroductoryPrice());

DPLogger.Log("getIntroductoryPricePeriodCycles is : " +
             subscriptionInfo.getIntroductoryPricePeriodCycles());

DPLogger.Log("getExpireDate is : " + subscriptionInfo.getExpireDate());

DPLogger.Log("getCancelDate is : " + subscriptionInfo.getCancelDate());

DPLogger.Log("getFreeTrialPeriod is : " + subscriptionInfo.getFreeTrialPeriod());

DPLogger.Log("getSubscriptionPeriod is : " + subscriptionInfo.getSubscriptionPeriod());

DPLogger.Log("getFreeTrialPeriodString is : " + subscriptionInfo.getFreeTrialPeriodString());

DPLogger.Log("getSkuDetails is : " + subscriptionInfo.getSkuDetails());

DPLogger.Log("getSubscriptionInfoJsonString is : " + subscriptionInfo.getSubscriptionInfoJsonString());

//——————

the logs that we get from Androind studio is:–

2019-08-02 18:02:35.613 6020-6118/? I/Unity: getProductId is : com.blahblah
   
    (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
2019-08-02 18:02:35.613 6020-6118/? I/Unity: getPurchaseDate is : 08/02/2019 10:03:29
   
    (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
2019-08-02 18:02:35.614 6020-6118/? I/Unity: isSubscribed is : True
   
    (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
2019-08-02 18:02:35.614 6020-6118/? I/Unity: isExpired is : False
   
    (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
2019-08-02 18:02:35.614 6020-6118/? I/Unity: isCancelled is : False
   
    (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
2019-08-02 18:02:35.614 6020-6118/? I/Unity: isFreeTrial is : False
   
    (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
2019-08-02 18:02:35.614 6020-6118/? I/Unity: isAutoRenewing is : True
   
    (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
2019-08-02 18:02:35.614 6020-6118/? I/Unity: getRemainingTime is : 00:00:53.9180000
   
    (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
2019-08-02 18:02:35.614 6020-6118/? I/Unity: isIntroductoryPricePeriod is : False
   
    (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
2019-08-02 18:02:35.614 6020-6118/? I/Unity: getIntroductoryPricePeriod is : 00:00:00
   
    (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
2019-08-02 18:02:35.614 6020-6118/? I/Unity: getIntroductoryPrice is : not available
   
    (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
2019-08-02 18:02:35.614 6020-6118/? I/Unity: getIntroductoryPricePeriodCycles is : 0
   
    (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
2019-08-02 18:02:35.614 6020-6118/? I/Unity: getExpireDate is : 08/02/2019 10:03:29
   
    (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
2019-08-02 18:02:35.615 6020-6118/? I/Unity: getCancelDate is : 01/01/0001 00:00:00
   
    (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
2019-08-02 18:02:35.615 6020-6118/? I/Unity: getFreeTrialPeriod is : 00:00:00
   
    (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
2019-08-02 18:02:35.615 6020-6118/? I/Unity: getSubscriptionPeriod is : 31.00:00:00
   
    (Filename: ./Runtime/Export/Debug.bindings.h Line: 43)
2019-08-02 18:02:35.615 6020-6118/? I/Unity: getFreeTrialPeriodString is :

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.

@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.

#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.

#if UNITY_ANDROID || UNITY_IOS || UNITY_STANDALONE_OSX
var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
// Get a reference to IAppleConfiguration during IAP initialization.
var appleConfig = builder.Configure<IAppleConfiguration>();
var receiptData = System.Convert.FromBase64String(appleConfig.appReceipt);
AppleReceipt receipt = new AppleValidator(AppleTangle.Data()).Validate(receiptData);

Debug.Log(receipt.bundleID);
Debug.Log(receipt.receiptCreationDate);
foreach (AppleInAppPurchaseReceipt productReceipt in receipt.inAppPurchaseReceipts) {
    Debug.Log(productReceipt.transactionIdentifier);
    Debug.Log(productReceipt.productIdentifier);
}
#endif

This works offline on iOS platform. For google we have:

var result = validator.Validate(e.purchasedProduct.receipt);
Debug.Log("Receipt is valid. Contents:");
foreach (IPurchaseReceipt productReceipt in result) {
    Debug.Log(productReceipt.productID);
    Debug.Log(productReceipt.purchaseDate);
    Debug.Log(productReceipt.transactionID);

    GooglePlayReceipt google = productReceipt as GooglePlayReceipt;
    if (null != google) {
        // This is Google's Order ID.
        // Note that it is null when testing in the sandbox
        // because Google's sandbox does not provide Order IDs.
        Debug.Log(google.transactionID);
        Debug.Log(google.purchaseState);
        Debug.Log(google.purchaseToken);
    }

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)

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.

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

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.