Search Unity

Resolved RestoreTransactions on iOS gives purchased consumables every time

Discussion in 'Unity IAP' started by SarperS, Oct 27, 2021.

  1. SarperS

    SarperS

    Joined:
    Mar 10, 2009
    Posts:
    824
    In not-yet-released iOS version of my game, I have a "restore purchases" UI button. I'm testing the game with a sandbox account. If I've purchased a consumable once, every time I tap on "restore purchases" the game gives me that consumable item, again and again. Unlimited soft currency. Below is my whole class of how I've implemented Unity Purchasing. Any pointers on why this is happening is greatly appreaciated.

    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. using UnityEngine.Purchasing;
    4. using UnityEngine.Purchasing.Security;
    5. using AE.Debugging;
    6.  
    7. public class InAppPurchasing : MonoBehaviour, IStoreListener {
    8.     public static InAppPurchaseSuccessfulEvent InAppPurchaseSuccessful;
    9.  
    10.     public GameObject MarketPanelGameObject;
    11.  
    12.     public MarketItemBase[] Products;
    13.  
    14.     public bool Initialized;
    15.     public IStoreController StoreController;
    16.  
    17.     private IExtensionProvider _extensionProvider;
    18.     private IAppleExtensions _appleExtensions;
    19.  
    20.     private bool _validatorInitialized;
    21.     private CrossPlatformValidator _validator;
    22.  
    23.     private bool _initializing;
    24.     private int _marketPanelGameObjectInstanceId;
    25.  
    26.     private void Awake() {
    27.         UIPanelTransitioner.UIPanelTransitionStarted += OnUIPanelTransitionStarted;
    28.         MarketItemDiamondPack.PurchaseInitiated += OnPurchaseInitiated;
    29.         MarketItemItemPack.PurchaseInitiated += OnPurchaseInitiated;
    30.         MarketItemRemoveAds.PurchaseInitiated += OnPurchaseInitiated;
    31.  
    32.         _marketPanelGameObjectInstanceId = MarketPanelGameObject.GetInstanceID();
    33.     }
    34.  
    35.     private void OnUIPanelTransitionStarted(GameObject panelGo, int tweenGroup) {
    36.         if(Initialized || _initializing) {
    37.             return;
    38.         }
    39.  
    40.         if(panelGo.GetInstanceID() != _marketPanelGameObjectInstanceId) {
    41.             return;
    42.         }
    43.  
    44.         ConfigurationBuilder b = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
    45.  
    46.         for(int i = 0; i < Products.Length; i++) {
    47.             if(!(Products[i] is IMarketInappPurchasable)) {
    48.                 AEDebug.LogWarning($"{Products[i].name} is not an IMarketInappPurchasable. Skipping.");
    49.                 continue;
    50.             }
    51.  
    52.             IMarketInappPurchasable p = Products[i] as IMarketInappPurchasable;
    53.             b.AddProduct(p.ProductId, p.ProductType);
    54.         }
    55.  
    56.         UnityPurchasing.Initialize(this, b);
    57.  
    58.         _initializing = true;
    59.     }
    60.  
    61.     public void OnInitialized(IStoreController controller, IExtensionProvider extensions) {
    62.         _initializing = false;
    63.         Initialized = true;
    64.  
    65.         StoreController = controller;
    66.         _extensionProvider = extensions;
    67.         _appleExtensions = _extensionProvider.GetExtension<IAppleExtensions>();
    68.  
    69. #if AE_DEBUG
    70.         AEDebug.Log("IAP::NOTIFICATION::Inapp purchasing initialized with below products");
    71.  
    72.         for(int i = 0; i < StoreController.products.all.Length; i++) {
    73.             Product p = StoreController.products.all[i];
    74.             AEDebug.Log($"id:{p.definition.id} title:{p.metadata.localizedTitle} price:{p.metadata.localizedPrice} price string:{p.metadata.localizedPriceString}");
    75.         }
    76. #endif
    77.     }
    78.  
    79.     public void OnInitializeFailed(InitializationFailureReason error) {
    80.         _initializing = false;
    81.         Initialized = false;
    82.  
    83.         AEDebug.LogError($"IAP::ERROR::{error}");
    84.     }
    85.  
    86.     public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason) {
    87.         AEDebug.LogError($"IAP::ERROR::PURCHASE::{product} purchase failed becaues {failureReason}");
    88.     }
    89.  
    90.     public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent) {
    91.         // If not one of the in-app purchase platforms, just mock as if the purchase was successful, this is here for editor testing
    92.         if(Application.platform != RuntimePlatform.Android && Application.platform != RuntimePlatform.IPhonePlayer && Application.platform != RuntimePlatform.OSXPlayer) {
    93.             InAppPurchaseSuccessful?.Invoke(purchaseEvent.purchasedProduct.definition.id);
    94.  
    95.             return PurchaseProcessingResult.Complete;
    96.         }
    97.  
    98.  
    99.         bool validPurchase = true;
    100.         IPurchaseReceipt[] receipts = new IPurchaseReceipt[0];
    101.  
    102.         if(!_validatorInitialized) {
    103.             _validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);
    104.             _validatorInitialized = true;
    105.         }
    106.  
    107.         try {
    108.             receipts = _validator.Validate(purchaseEvent.purchasedProduct.receipt);
    109.  
    110.             for(int i = 0; i < receipts.Length; i++) {
    111.                 if(Application.platform == RuntimePlatform.Android) {
    112.                     GooglePlayReceipt googleReceipt = receipts[i] as GooglePlayReceipt;
    113.  
    114.                     if(googleReceipt.purchaseState != GooglePurchaseState.Purchased) {
    115.                         validPurchase = false;
    116.                         AEDebug.Log($"IAP::NOTIFICATION::PURCHASE::Product refunded or purchase cancelled, not a valid purchase, product id: {googleReceipt.productID}");
    117.                         break;
    118.                     }
    119.                 }
    120.  
    121.                 AEDebug.Log("IAP::NOTIFICATION::PURCHASE::Product purchased with the following receipt:");
    122.                 AEDebug.Log($"id:{receipts[i].productID} date:{receipts[i].purchaseDate} transaction:{receipts[i].transactionID}");
    123.             }
    124.         } catch(IAPSecurityException) {
    125.             validPurchase = false;
    126.         }
    127.  
    128.         if(validPurchase) {
    129.             for(int i = 0; i < receipts.Length; i++) {
    130.                 InAppPurchaseSuccessful?.Invoke(receipts[i].productID);
    131.             }
    132.         } else {
    133.             AEDebug.LogError("IAP::ERROR::PURCHASE::Invalid purchase detected, receipt is not accepted");
    134.         }
    135.  
    136.  
    137.         return PurchaseProcessingResult.Complete;
    138.     }
    139.  
    140.     public void OnPurchaseInitiated(string productId) {
    141.         if(!Initialized) {
    142.             return;
    143.         }
    144.  
    145.         StoreController.InitiatePurchase(productId);
    146.     }
    147.  
    148.     public void OnRestorePurchasesButton() {
    149.         if(!Initialized) {
    150.             return;
    151.         }
    152.  
    153.         if(Application.platform != RuntimePlatform.IPhonePlayer && Application.platform != RuntimePlatform.OSXPlayer) {
    154.             return;
    155.         }
    156.  
    157.         _appleExtensions.RestoreTransactions(AppleRestorePurchasesCallback);
    158.     }
    159.  
    160.     private void AppleRestorePurchasesCallback(bool success) {
    161.         AEDebug.Log($"Restoring purchases callback return value is: {success} ");
    162.     }
    163. }
    IMarketInappPurchasable is an interface implemented by some of my market item classes.

    Code (CSharp):
    1.  
    2. public interface IMarketInappPurchasable {
    3.     string ProductId { get; set; }
    4.  
    5.     UnityEngine.Purchasing.ProductType ProductType { get; set; }
    6. }
    And upon checking I see those items that are given repeatedly are set as
    ProductType.Consumable
    as well.
     
    Last edited: Oct 27, 2021
  2. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    @SarperS Hmm, that is unexpected. What version of IAP are you using? The latest is 4.1.0, In App Purchasing library in Package Manager
     
  3. SarperS

    SarperS

    Joined:
    Mar 10, 2009
    Posts:
    824
    I was on 4.0.3, updated to 4.1.0 today and tested on the device. The problem persists (encountered on both versions).
     
    Last edited: Oct 27, 2021
  4. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    I will test this as soon as I can, hopefully today
     
  5. SarperS

    SarperS

    Joined:
    Mar 10, 2009
    Posts:
    824
    @JeffDUnity3D that is great news, thank you very much, looking forward to your results
     
  6. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    Restore works correctly for me on iOS. One note, you are calling every button with OnPurchaseInitiated in Awake which appears to auto-purchase each of those items. You might want to compare to the Sample IAP Project v2 here https://forum.unity.com/threads/sample-iap-project.529555/#post-6950270. I would encourage to sprinkle numerous Debug.Log statements (or your equivalent) throughout your code which will show in XCode https://forum.unity.com/threads/how-to-capturing-device-logs-on-ios.529920/
     
  7. SarperS

    SarperS

    Joined:
    Mar 10, 2009
    Posts:
    824
    Update: Forget all this. I simply forgot to put checks in place to not give user their NonConsumable items back on restore purchases if they already have them, I thought restore purchases would not return the same items when user already had them. See, one of the non-consumables I have has a pack of items, containing diamonds. I thought it was giving back the consumable diamonds product.


    Code (CSharp):
    1. private void Awake() {
    2.         UIPanelTransitioner.UIPanelTransitionStarted += OnUIPanelTransitionStarted;
    3.         MarketItemDiamondPack.PurchaseInitiated += OnPurchaseInitiated;
    4.         MarketItemItemPack.PurchaseInitiated += OnPurchaseInitiated;
    5.         MarketItemRemoveAds.PurchaseInitiated += OnPurchaseInitiated;
    6.         _marketPanelGameObjectInstanceId = MarketPanelGameObject.GetInstanceID();
    7.     }
    Here? I'm subscribing to their PurchaseInitiated events. When they are clicked, they raise an event of the below delegate.

    public delegate void MarketItemInAppInitiatedEvent(string productId);


    So they send their productID. So their callback is OnPurchaseInitiated. I'm not calling anything in awake? And nothing is auto-purchased either.

    T̶h̶e̶ ̶i̶m̶p̶l̶e̶m̶e̶n̶t̶a̶t̶i̶o̶n̶ ̶i̶s̶ ̶c̶o̶r̶r̶e̶c̶t̶.̶ ̶T̶h̶e̶ ̶o̶n̶l̶y̶ ̶p̶r̶o̶b̶l̶e̶m̶ ̶i̶s̶ ̶"̶r̶e̶s̶t̶o̶r̶e̶ ̶p̶u̶r̶c̶h̶a̶s̶e̶s̶"̶ ̶b̶u̶t̶t̶o̶n̶ ̶g̶i̶v̶e̶s̶ ̶c̶o̶n̶s̶u̶m̶a̶b̶l̶e̶ ̶i̶t̶e̶m̶s̶ ̶b̶a̶c̶k̶ ̶e̶v̶e̶r̶y̶ ̶t̶i̶m̶e̶.̶ ̶S̶o̶ ̶i̶f̶ ̶I̶'̶v̶e̶ ̶p̶u̶r̶c̶h̶a̶s̶e̶d̶ ̶a̶ ̶c̶o̶n̶s̶u̶m̶a̶b̶l̶e̶ ̶i̶t̶e̶m̶ ̶c̶a̶l̶l̶e̶d̶ ̶3̶0̶0̶ ̶d̶i̶a̶m̶o̶n̶d̶s̶,̶ ̶u̶p̶o̶n̶ ̶c̶l̶i̶c̶k̶i̶n̶g̶ ̶m̶y̶ ̶R̶e̶s̶t̶o̶r̶e̶ ̶P̶u̶r̶c̶h̶a̶s̶e̶s̶ ̶i̶t̶e̶m̶,̶ ̶[̶I̶C̶O̶D̶E̶]̶O̶n̶R̶e̶s̶t̶o̶r̶e̶P̶u̶r̶c̶h̶a̶s̶e̶s̶B̶u̶t̶t̶o̶n̶[̶/̶I̶C̶O̶D̶E̶]̶ ̶i̶s̶ ̶c̶a̶l̶l̶e̶d̶,̶ ̶a̶n̶d̶ ̶i̶t̶ ̶s̶i̶m̶p̶l̶y̶ ̶d̶o̶e̶s̶ ̶[̶I̶C̶O̶D̶E̶]̶_̶a̶p̶p̶l̶e̶E̶x̶t̶e̶n̶s̶i̶o̶n̶s̶.̶R̶e̶s̶t̶o̶r̶e̶T̶r̶a̶n̶s̶a̶c̶t̶i̶o̶n̶s̶(̶A̶p̶p̶l̶e̶R̶e̶s̶t̶o̶r̶e̶P̶u̶r̶c̶h̶a̶s̶e̶s̶C̶a̶l̶l̶b̶a̶c̶k̶)̶[̶/̶I̶C̶O̶D̶E̶]̶.̶ ̶T̶h̶i̶s̶ ̶g̶i̶v̶e̶s̶ ̶m̶e̶ ̶3̶0̶0̶ ̶d̶i̶a̶m̶o̶n̶d̶s̶ ̶e̶v̶e̶r̶y̶ ̶t̶i̶m̶e̶ ̶I̶ ̶d̶o̶ ̶t̶h̶i̶s̶.̶
     
    Last edited: Jun 7, 2022