Search Unity

[iOS] ProcessPurchase getting called multiple times for expired subscriptions

Discussion in 'Unity IAP' started by Nicolas1212, Nov 2, 2018.

  1. Nicolas1212

    Nicolas1212

    Joined:
    Dec 18, 2014
    Posts:
    139
    I'm currently testing IAPs with subscriptions, and I'm running into this behaviour when testing on iOS

    My basic flow is:
    • User purchases subscription
    • Receipt is sent to the server and validated
    • ConfirmPendingPurchase() is called on the client
    • On next app startup, I check all products. If one is of type ProductType.Subscription and has a receipt, then I send it to the server to validate that the subscription is active
    In the iOS receipt, it stores each individual purchase/renewal, like so:

    Code (csharp):
    1.  
    2. "in_app": [
    3.       {
    4.         "quantity": "1",
    5.         "product_id": "...",
    6.         "transaction_id": "100...860",
    7.         "original_transaction_id": "100...130",
    8.         "purchase_date": "2018-10-31 16:20:06 Etc/GMT",
    9.         "purchase_date_ms": "15410028060 00",
    10.         "purchase_date_pst": "2018-10-31 09:20:06 America/Los_Angeles",
    11.         "original_purchase_date": "2018-10-31 16:17:51 Etc/GMT",
    12.         "original_purchase_date_ms": "1541002671000",
    13.         "original_purchase_date_pst": "2018-10-31 09:17:51 America/Los_Angeles",
    14.         "expires_ date": "2018-10-31 16:23:06 Etc/GMT",
    15.         "expires_date_ms": "1541002986000",
    16.         "expires_date_pst": "2018-10-31 09:23:06 America/Los_Angeles",
    17.         "web_order_line_item_id": "100...209",
    18.         "is_trial_period": "false",
    19.         "is_in_intro_offer_period": "false"
    20.       },
    21.       {
    22.         "quantity": "1",
    23.         ...
    24.         "purchase_date": "2018-10-31 16:23:06 Etc/GMT",
    25.         "expires_date": "2018-10-31 16:26:06 Etc/GMT",
    26.         ...
    27.       },
    28.    ...
    Somewhat awkward, but understandable.

    However, I'm finding that ProcessPurchase is getting called for each element in the "in_app" array, even if it's expired and/or previously validated. If I've 20 objects in the array, I'll get 20 notifications.

    In each notification, the same Product object (i.e. same hashCode) is sent, though the transactionID is different

    Is this normal behaviour? I'm using UnityIAP 1.21.0

    Thanks
     
  2. JeffDUnity3D

    JeffDUnity3D

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

    Nicolas1212

    Joined:
    Dec 18, 2014
    Posts:
    139
    Hi @JeffDUnity3D,

    I do use the SubscriptionManager to get different info on the subscription itself, however I still need to pass it to the server in order to validate the actual receipt (local validation isn't foolproof), but mainly so I can gift/update the player with the subscription benefits.

    UnityIAP, no? I don't call it manually if that's what you mean. It's the normal flow when Unity is notifying me of a purchase.

    It's somewhat complicated the whole purchase process, as what used to be:
    • Receive the ProcessPurchase callback
    • Validate the receipt locally with CrossPlatformValidator
    • Send the receipt to the server to revalidate it there
    • Apply the purchase, and return the results
    to
    • Receive the ProcessPurchase callback (times the number of subscription items in the receipt)
    • Validate the receipt locally with CrossPlatformValidator
    • On iOS, go through the IPurchaseReceipt array that's returned and see if each product is a subscription, is expired, or has already been treated (we validate subscriptions on startup)
    • If all the items have already been treated, mark the purchase as completed
    • If there's at least one items that's valid, continue the flow as normal
    • Repeat for all the other notifications
    It makes it somewhat... unsteady :) Due to the async nature of ProcessPurchase, it's hard for me to say "Okay, now I've received all the purchase notifications, let's see which ones are valid." I need to treat them one-by-one, and the more logic there is in the flow, the more chance there is for an error to creep in, or a race-condition in how we treat stuff to throw everything sideways.

    Do you mean just marking it complete immediately?

    Basically, I get this behaviour when there's an active subscription in the purchase list. If there's one active, then I'll get a notification for each element in the receipt (on a side note, we don't have any non-consumable/non-subscription products, so I don't know if this behaviour still exists for other purchases that stay permanently in the receipt).

    If all subscription entries are expired, then I haven't had it trigger.

    It doesn't trigger on Android either (though on Android, there's only ever one entry in the receipt).
     
  4. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    @Nicolas1212 Understood, thank you for the flow description. I was asking if your server validation somehow triggered ProcessPurchase or changed the behavior, so I wanted to understand your flow. So for clarity, when do you receive the ProcessPurchase callback, is this during a first-time purchase or during Restore? I'm referring to this statement. I'm basically looking for exact steps to reproduce here:

    • Receive the ProcessPurchase callback (times the number of subscription items in the receipt)
     
  5. Nicolas1212

    Nicolas1212

    Joined:
    Dec 18, 2014
    Posts:
    139
    Hi @JeffDUnity3D,

    Full flow description - let me know if there's something you need more info on, or if something's not clear. This is all on iOS
    • Launch the app, make a purchase of a weekly recurring subscription (staging, where 1W = ~3 mins)
    • ProcessPurchase is called as normal
    • The receipt is validated locally with CrossPlatformValidator
    • The receipt is sent to the server, where it's sent to Apple, the return validated, and the subscription applied to the player
    • When the client receives the result from the server, ConfirmPendingPurchase is called
    • (this is not really necessary for the flow, but helps understand what we're doing) Each subsequent time the app is opened:
      • Once the store is inited, we go through the products.all array
      • For any products that are type ProductType.Subscription, we check if they have a receipt
      • If they have a receipt, it's sent to the server to be validated and the subscription extended if necessary (in our game logic)
      • If they don't have a receipt, then we tell the server to clear the subscription on the player
    • Let the subscription expire - this normally takes about 6 renewals the first time, or around 20 minutes or so
    • You should be left with a receipt with multiple entries for each renewal, detailing the period and the transactionID of each re-billing
    • Repurchase the subscription
    • Flow validation is as before
    • Close the app, and re-open it *while the subscription is still valid* (i.e. within 3 minutes on staging)
    • When the app opens, we check for a valid subscription as before
    • However, this time, ProcessPurchase is called for each element (subscription renewal) in the receipt, even if it's expired, or been previously marked as completed
      • Each time ProcessPurchase is called, the Product object passed is identical (same hashcode), but the transactionID is different, representing one of the transactions in the receipt
      • This means that if you have 7 transactions in your receipt for the subscription (one active, and 6 expired), ProcessPurchase will be called 7 times, once for each of them
    I originally thought that it was because multiple renewals had passed since I'd launched it last (the nature of making code changes, building Unity, the building Xcode meant that it'd pass the 3 minute barrier), but even when they're marked as Complete, if there's an active transaction in the receipt, they all get called.
     
  6. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    In speaking with the IAP team, this currently expected behavior. But this may change based on your feedback, and may be included in a future version.
     
  7. Nicolas1212

    Nicolas1212

    Joined:
    Dec 18, 2014
    Posts:
    139
    Hi @JeffDUnity3D,

    If this is expected behaviour, it'd be great to have an example in the IAP docs mentioning it and showing the best way to deal with it, especially because each notification includes the full receipt each time (so in the previous example, ProcessPurchase is called 7 times, each passing a receipt with 7 items in it).

    Is it:
    • Get notified of ProcessPurchase
    • Take the transactionID and find the right element in the receipt
    • If it's a subscription, and if it's expired, just mark the purchase as Complete directly?
    In the case where you can't mark it Complete directly (i.e. as the return of ProcessPurchase), given that each time you're notified, the Product object is identical, how does that impact calling ConfirmPendingPurchase( product )? If one element in the receipt is valid, and you send it to your server, by the time you get the return, you might have received another ProcessPurchase, which has changed the transactionID on your Product (albeit while keeping the same receipt).

    Normally server could should handle the possibility of having multiple transactions in the receipt - should we ignore all that don't match the main transactionID passed?

    Thanks for having looked into this btw.
     
  8. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    Engineering is asking that you try with 1.20.1 (the latest version which is more recent than 1.21) where a fix was implemented to better handle apple subscriptions