[iOS] Duplicate Transaction error on purchases

We are getting an annoying “duplicate transaction” error on iOS when testing our sandbox builds.

We are not sure if that’s caused by an inconsistency related to “Restore Transactions” or if the error is related to something else.

It is happening randomly in our iOS test devices and we couldn’t figure out how to fix that, so we suppose it’s a SDK bug.

I have attached a few logs to this post. The logs are ordered by sequential sessions on the same device.

6635563–756895–1) Duplicate Purchases error after Restore Transactions.txt (2.91 MB)
6635563–756898–2) Infinite Loading on Restore Transactions.txt (658 KB)
6635563–756901–3) Open app and try to purchase results in Duplicate Transaction Detected.txt (1.08 MB)

1 Like

The logs are huge, what are we looking for? Are there exceptions or errors in the logs? Please filter, and please provide steps to reproduce. What type of product are you trying to purchase after the Restore? You can only purchase non-consumables and subscriptions once. To be fair, we’ve heard of this behavior from another user also, but I’m not able to reproduce. Also share the version of IAP and Unity that you are using.

Same issue here. It happened randomly, some of our testers met this duplicate transaction issue, some of them didn’t. I think it’s Apple’s problem.

Is this a recent issue? Did Apple change anything? Does anyone know any update here? Because we too are facing this issue. Our last build, things looked fine. Today when we again tested the build for something else, we got stuck in the purchase screen, due to DuplicateTransaction issue.

I tested for a while. After some time it goes away, and I am able to buy the product. Seems like IAppleExtension.RestoreTransaction() seems to initiate something that takes some while to complete. Although I receive the boolean, still there seems to be some process running that prevents the current product from purchasing.

2 Likes

We have heard of various reports of similar behavior after Restore on iOS. I haven’t been able to reproduce, but we are looking into it.

@JeffDUnity3D To replicate this, we need to have a restore button. On clicking the restore button, call the RestoreTransaction method.

  1. We need to click on the restore button when there are no products to restore.
  2. Immediately, click on a product button and call InitiatePurchase()
  3. This should result in PurchaseFailed error with DuplicateTransaction. If we try again after some time, it will succeed.

Got it, thanks for sharing. So the user has never purchased the product before (or any product), yet is receiving Duplicate Transaction? This only happens on the very first purchase by the user? Is this a consumable product?

We are using a subscription product. I am not sure if this happens for other types of products as well. During testing, I can replicate this once the subscription is expired, and I click on restore purchase. before clicking on the product

1 Like

So the product was purchases previously if the subscription is expired. Please include all steps for clarity. There does seem to be an issue, but we need clear steps to reproduce.

To clarify, this will occur whenever we try to restore a purchase, when there is nothing to restore. So there are two likely scenarios-

  1. First time we click on Restore button, there will be no products purchased yet. After this if we try to buy something, the duplicatetransaction error is shown
  2. If we buy a subscription, and after a few minutes, it expires (sandbox mode). Now if we try to restore, again there is nothing to restore. After this if we try to buy the subscription again, the duplicatetransaction error will be shown.

Hope I am clear here. I might be wrong about how restore works, since this is the first time I am implementing this. But I have followed the documentations as far as possible, and there is not much to do other than just call RestoreTransaction and wait for a result (which will always be returned).

1 Like

Yes, thank you for the additional clarity! We will check.

@JeffDUnity3D As a quick hotfix is there any way to check if there are any products to restore? This way I can ensure that restore runs only when there are actually products to restore. Please this is a bit urgent issue.

Our products were consumables, but still got this duplicate transaction issue. We were using Apple Sandbox to test the purchases, so it might be a Sandbox-only issue. Unfortunately, we didn’t know how to reproduce the issue, it happened randomly, once it happened, it happened all the time.

No, there isn’t another way that I am aware of to check to see if there are products to restore.

@JeffDUnity3D , any updates on this issue? Were you able to replicate this or look into it? This is a pretty major issue since it will not let us proceed further in the game as the user is not able to buy anything.

@better_walk_away were you able to test if this issue occurs in production as well?

@JeffDUnity3D I seem to have found another issue with Restore Purchase. The restore purchase method appears to be triggering the ProcessPurchase callback multiple times. I am not sure why. We just have two subscription products, so as far as I understand, the restore purchase is called for each product. So at the most there should be two calls.

However somehow the ProcessPurchase seems to be triggering multiple times for me. I previously thought, since I was calling Pending status and confirming it after validation, there must have been some loop running which was resulting in multiple calls. However, after checking and changing the code to call Complete status for RestorePurchase, I can still see that multiple calls to ProcessPurchase are happening. I’ll share my code below for your reference.

        public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
        {
            if (isRestoring)
            {
                Debug.Log($"Restoring: {e.purchasedProduct.definition.id} with transaction id: {e.purchasedProduct.transactionID}");
                Debug.Log($"Restored products count: {++restoreCount}");
                //isRestoring = false;
                OnRestorePurchase?.Invoke(e.purchasedProduct);
                return PurchaseProcessingResult.Complete;
            }

            Debug.LogError("Purchase Pending :" + e.purchasedProduct.definition.id);
            OnPurchaseProcessing?.Invoke(e.purchasedProduct);
            return PurchaseProcessingResult.Pending;
        }
        public void RestorePurchases()
        {
            Debug.Log("Restore IAP link");
            if (!IsInitialized())
            {
                Debug.Log("RestorePurchases FAIL. Not initialized.");
                OnRestoreComplete?.Invoke(false);
                return;
            }
            if (Application.platform == RuntimePlatform.IPhonePlayer ||
                Application.platform == RuntimePlatform.OSXPlayer)
            {
                Debug.Log("RestorePurchases started ...");
                var apple = _extensionProvider.GetExtension<IAppleExtensions>();

                isRestoring = true;

                apple.RestoreTransactions((result) =>
                {
                    Debug.Log("RestorePurchases continuing: " + result + ". If no further messages, no purchases available to restore.");
                    OnRestoreComplete?.Invoke(result);
                });
            }
            else
            {
                Debug.Log("RestorePurchases FAIL. Not supported on this platform. Current = " + Application.platform);
                OnRestoreComplete?.Invoke(false);
            }
        }

The last time I ran the app, the Restored products count was around 135. This is causing the loading time to take a very long time, and more critically the purchase flow to behave very unexpectedly.

I am really a bit worried right now, since I have had multiple issues with Unity IAP. This is the 3rd or 4th major issue that I am facing with subscription. A couple of them, you have already mentioned that they are being looked into. Another one, related to testing times, you’ve said that you cannot control. However this issue and the previous issue with the duplicate transaction are the major issues that is really causing me a lot of frustration. I am not being able to submit the game to the app store due to these restore issues.
How can there be so many bugs in this? Am I doing something really wrong with my code structure? Or are there really these many game-breaking bugs in subscription module of Unity IAP? How are there so many games out there created with Unity with subscription? Is there any way I can contact one of you guys and understand what could be causing these issues?

2 Likes

Though we are also facing this duplicate transaction issue when testing using TestFlight and the Sandbox environment, we decided to roll out the new version of the game regardless, we rolled out the new version of the game on Christmas Eve. So far, we haven’t received any error messages regarding iOS IAP, so I think it’s a Sandbox-only issue.

2 Likes

@better_walk_away Thanks for that information. If its only in Sandbox mode, I can convince the QA team to pass the build. However, I feel this is something that needs to be addressed on priority by Unity, since this is a potential blocker as user will neither be able to restore, nor able to purchase a new subscription.

1 Like

We have no control over the Apple Sandbox environment. You might expect multiple callbacks if you are using their test environment and the subscription renewed several times within 15 minutes for example.

@JeffDUnity3D Can you give me an idea on how I can handle this scenario? How can I control which product is being restored? Will the first product be the correct item to restore or the last product?

Also, it would be very helpful if you could help me understand how this code works:

                var apple = _extensionProvider.GetExtension<IAppleExtensions>();
                isRestoring = true;
                apple.RestoreTransactions((result) =>
                {
                    Debug.Log("RestorePurchases continuing: " + result + ". If no further messages, no purchases available to restore.");
                    OnRestoreComplete?.Invoke(result);
                });

As per my understanding, the OnRestoreComplete should be called once all the products have completed ProcessPurchase. But I am not sure if this is 100% valid. Sometimes OnRestoreComplete is called after all products, and sometimes ProcessPurchase will still be called after OnRestoreComplete, I think. I keep getting a bit lost with my async logs, to be honest. I just want to restore one product and then discard the others. So I am thinking of keeping some boolean flag. But I am not getting where exactly to add it.