Android IAP - strange receipt payload after upgrading to Unity IAP 1.23.3

Hello,

Recently we upgraded Unity IAP’s version to 1.23.3, and when we released the app, some small amount of Android users have problem when purchasing consumable items. We validate receipts with our backend server, but recently our server rejected the receipt because some of the fields were missing, and somehow it contained some old information. Details below.

This is the correct receipt which was processed successfully.

{
    "Store":"GooglePlay",
    "TransactionID":"GPA.****-****-****-*****",
    "Payload":"{
        \"json\":\"{
            \\\"orderId\\\":\\\"GPA.****-****-****-*****\\\",
            \\\"packageName\\\":\\\"com.package.name\\\",
            \\\"productId\\\":\\\"com.package.name.item1\\\",
            \\\"purchaseTime\\\":1594811344959,
            \\\"purchaseState\\\":0,
            \\\"developerPayload\\\":\\\"{\\\\\\\"developerPayload\\\\\\\":\\\\\\\"XXXXXX==\\\\\\\\n\\\\\\\",\\\\\\\"is_free_trial\\\\\\\":false,\\\\\\\"has_introductory_price_trial\\\\\\\":false,\\\\\\\"is_updated\\\\\\\":false,\\\\\\\"accountId\\\\\\\":\\\\\\\"\\\\\\\"}\\\",
            \\\"purchaseToken\\\":\\\"thisisPurchaseToken-abCdeF123456.....\\\"}\",
        \"signature\":\"thisisthesignature1234456.....==\",
        \"skuDetails\":\"{
            \\\"skuDetailsToken\\\":\\\"SkUToKeNSkUToKeN....\\\",
            \\\"productId\\\":\\\"com.package.name.item1\\\",
            \\\"type\\\":\\\"inapp\\\",
            \\\"price\\\":\\\"¥1,234\\\",
            \\\"price_amount_micros\\\":1234000000,
            \\\"price_currency_code\\\":\\\"JPY\\\",
            \\\"title\\\":\\\"product title\\\",
            \\\"description\\\":\\\"product description\\\"
            }\",
        \"isPurchaseHistorySupported\":true,
        \"isOwned\":true
    }"
}

After the user successfully purchased an item with the receipt above, on the other day the user tried to purchase another same item, but the receipt sent to the server was like this:
(Ignore the different number of backslashes at the payload value; somehow our error reporting tool stripped one level of escaping)

{
  "Store": "GooglePlay",
  "TransactionID": "thisisPurchaseToken-abCdeF123456.....",
  "Payload": "{
    "json":"{
      \"productId\":\"com.package.name.item1\",
      \"purchaseToken\":\"thisisPurchaseToken-abCdeF123456.....\",
      \"purchaseTime\":1594811344959,
      \"developerPayload\":\"{\\\"developerPayload\\\":\\\"XXXXXX==\\\\n\\\",\\\"is_free_trial\\\":false,\\\"has_introductory_price_trial\\\":false,\\\"is_updated\\\":false,\\\"accountId\\\":\\\"\\\"}\"}",
    "signature": "thisisthesignature1234456.....==",
    "skuDetails": "{
        \"skuDetailsToken\":\"SkUToKeNSkUToKeN....\",
        \"productId\":\"com.package.name.item1\",
        \"type\":\"inapp\",
        \"price\":\"¥1,234\",
        \"price_amount_micros\":1234000000,
        \"price_currency_code\":\"JPY\",
        \"title\":\"product title\",
        \"description\":\"product description\
        "}",
    "isPurchaseHistorySupported":true,
    "isOwned": true
    }"
}

There are some strange things here:

  • The user was trying to initiate a new purchase, but somehow the returned receipt has an old and consumed purchaseToken included, so Google Play Store’s purchases.product.get API responded it with consumptionState = 1.
  • There’s no orderId (GPA-***) inside the payload, and Unity inserted purchaseToken instead of the orderId
  • Some fields are missing in the Payload: orderId, packageName, and purchaseState.

Any help would be appreciated.

You would want to review the Release Notes for this version

Problem solved. Read the 1.23.4 release notes and realized that starting from 1.23.2 it calls queryPurchaseHistoryAsync() which returns a slightly different receipt (PurchaseHistoryRecord)…

1 Like

Hi, I read the release note but still have some questions. Based on my test, we will receive a receipt without packageName even when we canceled a purchase and the receipt will be delivered to ProcessPurchase method. Is this the correct behabiour? What is the purpose of this design? As the way I see it, ProcessPurchase should only be called when a purchase is in Pending but for a canceled purchase, why did UnityIAP send the receipt back again and without packageName? Plus, I didn’t set aggressivelyRecoverLostPurchases to true.

Hi, hadididi, can you tell me how did you deal with these strange receipts?

Hi, after some testing and questions from our users, I think this is how purchase recovery works. Our game has only consumable products though, so I don’t know how it behaves for other types of purchase.

> ProcessPurchase should only be called when a purchase is in Pending but for a canceled purchase, why did UnityIAP send the receipt back again and without packageName?

If you are trying to buy a product that have been purchased and consumed before and you cancel it after the Google Play’s payment dialog appears (before paying it), the next time you launch the game, IAP will call Android’s queryPurchaseHistory API and it will call processPurchase with the last successful and consumed receipt, without the packageName included (and transactionID becomes purchaseToken, not orderId). (e.g., if you are trying to buy a consumable product which you had bought 6 months ago, then that 6-month old receipt will be passed to processPurchase on next launch.) It seems that any kind of purchase error (including cancelling payment) will trigger the purchase recovery process.

> Plus, I didn’t set aggressivelyRecoverLostPurchases to true.

Actually, I’m in the same situation with you. I realized that option recently, and I didn’t set that option. I am using 1.23.3 which states that it should be “opt-in”, but somehow IAP just acts like it was set to true. I’m sure this is a bug…

> can you tell me how did you deal with these strange receipts?

All receipts are validated server-side, so if the incoming receipt has been consumed and existed in our receipt database, the server just ignore it and send a status code to the front-end, which states that the front-end should ignore and don’t do anything, don’t display any message to the user.
I also save the receipt with the purchaseToken as the primary key, as google recommended, and I actually ignored the transactionID field, since it may change between orderId and purchaseToken.

Hope this helps.