Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restore from redeemed codes does not work (iOS) #51

Closed
Sebastian1989101 opened this issue Jun 7, 2017 · 57 comments
Closed

Restore from redeemed codes does not work (iOS) #51

Sebastian1989101 opened this issue Jun 7, 2017 · 57 comments

Comments

@Sebastian1989101
Copy link
Contributor

If you are creating an issue for a BUG please fill out this information. If you are asking a question or requesting a feature you can delete the sections below.

Failure to fill out this information will result in this issue being closed. If you post a full stack trace in a bug it will be closed, please post it to http://gist.github.com and then post the link here.

Bug Information

Version Number of Plugin: 1.1.0.35-beta
Device Tested On: Multiple iOS devices
Simulator Tested On: -
Version of VS: 15.2 (26430.12) Release
Version of Xamarin: 2.3.4.247
Versions of other things you are using: Mapsui 1.0.9

Steps to reproduce the Behavior

Redeem multiple an IAP codes on iTunes and try to restore it on the iOS device (does not happend all the time. It happens in 1/20 tries).

Expected Behavior

On restore there should be each product that was redeemed with a code or purchased.

Actual Behavior

The last redeemed code returns multiple times. So if you have a code for "Unlock XY" and one for "Remove Ads" the last redeemed code is returned for each redeemed code. In this case if you redeem "Unlock XY" first and "Remove Ads" than, you got 2x "Remove Ads" on the restore function.

Code snippet

        private async void OnRestorePurchasesCommandExecute()
        {
            try
            {
                ProcessIsRunning = true;
                var restoredProducts = new List<string>();

                var connection = await CrossInAppBilling.Current.ConnectAsync();
                if (!connection)
                {
                    await page.DisplayAlert("Error", "Error while connecting to Store.", "OK");
                    return;
                }

                var purchases = (await CrossInAppBilling.Current.GetPurchasesAsync(ItemType.InAppPurchase, new InAppBillingVerifyPurchase()))?.ToArray();
                if (purchases != null && purchases.Any())
                {
                    foreach (var purchase in purchases)
                    {
                        if (Products.Any(p => p.ID == purchase.ProductId))
                        {
                            context.Send(async c =>
                                {
                                    if (purchase.State == PurchaseState.Purchased || purchase.State == PurchaseState.Restored)
                                    {
                                        var product = Products.First(p => p.ID == purchase.ProductId);
                                        product.Purchased = true;

                                        if (await MarkProductAsPurchased(purchase.ProductId))
                                            restoredProducts.Add(product.Name);
                                    }
                                }, null);
                        }
                        else
                        {
                            await page.DisplayAlert("Error", $"Can not identify product with ID \"{purchase.ProductId}\".", "OK");
                        }
                        
                        if (purchase.ProductId == PurchaseableProduct.ALL_PAGES && (purchase.State == PurchaseState.Purchased || purchase.State == PurchaseState.Restored))
	                    {
		                    foreach (var product in Products.Where(p => p.IsPageUnlock))
			                    context.Send(c => { product.Purchased = true; }, null);
	                    }

                        await DependencyService.Get<IWebclient>().SendPurchaseReportAsync(purchase);
                    }

                    await page.DisplayAlert("Status", restoredProducts.Any() ? $"Products have been successfully restored. {Environment.NewLine}{Environment.NewLine}Restored: {string.Join(", ", restoredProducts)}" : "Products could not be restored.", "OK");
                }
            }
            catch (Exception ex)
            {
                await page.DisplayAlert("Error", ex.Message, "OK");
            }
            finally
            {
                await CrossInAppBilling.Current.DisconnectAsync();
                ProcessIsRunning = false;
            }
        }

Screenshotst

Here is entry in my DB from the PurchaseReport-Method I built. The customer gets 2x the last product he restored. http://puu.sh/wdD3q/55a9cd9e74.png

@jamesmontemagno
Copy link
Owner

So I can create this what types of products did you create in iTunes connect?

It sounds like you did this:

1.) Create 2 Non-Consumables
2.) Create a coupon for both of them
3.) Redeem coupon for both of them
4.) Hit "Restore" to see what has come down from Apple

Assumption is that you get both of them, but you seem to be getting a duplicate.

May be related to: #29 ?

@Sebastian1989101
Copy link
Contributor Author

1 - 4: Yes exactly this way.

Yes I get a duplicate - in this case both with the PurchaseState "Purchased". So I don't see the relation to #29 because I don't get a Deffered result. The strange thing is that it worked on some devices like my own but fails with this result on others. I got the reports from my customers and decided to log the response with my web API.

@Sebastian1989101
Copy link
Contributor Author

Is there any update on this? I got more and more requests from my users about failed restores because the restore functions gets the same product id multiple times instead of each purchased product by itself (iOS).

@jamesmontemagno
Copy link
Owner

I haven't had time to look at it at all to be honest as I don't use coupons, but do plan to look into it.

So if you have:
"productid1" and "productid2"

If I buy them out right from the app then I get back 2 purchases for "productid1" and "productid2"

If I redeem both of them as promo codes then it gets back
"productid1" and "productid1" ?

@Sebastian1989101
Copy link
Contributor Author

It seems not to happen on all devices. I implement a web log so that when a user hit's restore I see what is restored and there are about 2-3% from my users that gets the last purchase multiple times instead of each once. Because of the high amount of this error I guess it is not because of the gift codes (I only give about 30 away and have about 482 different devices in my log with this problem yet).

The purchase or redeem "Product1", "Product2" and "Product3". Now they want to restore and sometimes it get's 3x "Product3" - the last purchase / redeem - instead of each once. If it happens on a device it would not change (two of the users contacted me directly and tried it multiple times). I don't know what happens exactly there but for me it looks like a problem where the ReSharper would say "access to modified closure".

@jamesmontemagno
Copy link
Owner

Got it hmmmmm interesting. I am looking through the code now and not seeing anything. I just do this to convert:

var p = transaction.OriginalTransaction;
            if (p == null)
                p = transaction;

            if (p == null)
                return null;

            return new InAppBillingPurchase
            {
                TransactionDateUtc = NSDateToDateTimeUtc(p.TransactionDate),
                Id = p.TransactionIdentifier,
                ProductId = p.Payment?.ProductIdentifier ?? string.Empty,
                State = p.GetPurchaseState()
            };

Looks valid

@jamesmontemagno
Copy link
Owner

I'm looking through this... https://developer.xamarin.com/guides/ios/platform_features/in-app_purchasing/transactions_and_verification/

The only thing I see is a switch for restored. Which I have added... But still strange.

@jamesmontemagno
Copy link
Owner

The code is small, so maybe just add that to your IOS app and try it out and see what comes from it.

I only have apps with a single item currently so hard for me to test.

@Sebastian1989101
Copy link
Contributor Author

I've tried to create this problem on any of my devices (13 in total for iOS) with about 30 accounts and I was not able to create the issue at least once... I just hear it in the reports and see it in the logs that the user gets for a single restore multiple times the same product id.

I will implement the code tomorrow to my app and let one user test it where I know the problem occurred.

@jamesmontemagno
Copy link
Owner

gotcha, i also deployed a new pre-release that has my switch casing on the restored... maybe that was being ignored before?

@jamesmontemagno
Copy link
Owner

jamesmontemagno commented Jun 29, 2017

Also, are these your apps: https://itunes.apple.com/us/app/breath-companion/id1218936949?mt=8 https://play.google.com/store/apps/details?id=com.SoftwareNotion.BreathCompanion?

Very cool! We are all huge zelda fans :)

Let's make sure we get this working for sure. I can install the iOS app and try to reproduce too. Feel free to email me motz (at) microsoft.com

@Sebastian1989101
Copy link
Contributor Author

Thats exactly the app where I have the issue (and a few other issues on Android as well since the latest version but thats not because of this plugin ^^). Just uploaded a test version with the changes to TestFlight. As soon as the test version is approved by Apple, I can give you feedback if this helps to resolve the issue.

@jamesmontemagno
Copy link
Owner

sounds good! Let me know :)

@Sebastian1989101
Copy link
Contributor Author

I got the first responses from my beta users about the issue. So far, there is no problem on iOS for users that worked before as well. But the users that got before multiple times the last purchase id, not gets an NullReferenceException on restore. I just added a push to my web logging to get the entire stack trace. All the information I currently have is that the Exception occurs somewhere since the update on the restore function - but only for users with the problem before.

@jamesmontemagno
Copy link
Owner

Interesting.... yeah would like to see the stack trace, maybe it is just something when i am converting it and it could be a possible fix. these things are so hard to test.

@Sebastian1989101
Copy link
Contributor Author

So far still no exception from iOS in my log... But I got one from Android that seems to be the same (?) - a NullReferenceException when the restore is triggered:

System.NullReferenceException: Object reference not set to an instance of an object
  at Plugin.InAppBilling.InAppBillingImplementation+<>c__DisplayClass25_0+<<GetPurchasesAsync>b__0>d.MoveNext () [0x00047] in <c0efaab9a8d94a42973f5bd70672a31f>:0 
--- End of stack trace from previous location where exception was thrown ---
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in <5fa4890be57f49d5a93452df59075ae8>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) [0x0003e] in <5fa4890be57f49d5a93452df59075ae8>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) [0x00028] in <5fa4890be57f49d5a93452df59075ae8>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) [0x00008] in <5fa4890be57f49d5a93452df59075ae8>:0 
  at System.Runtime.CompilerServices.TaskAwaiter`1[TResult].GetResult () [0x00000] in <5fa4890be57f49d5a93452df59075ae8>:0 
  at Plugin.InAppBilling.InAppBillingImplementation+<GetPurchasesAsync>d__24.MoveNext () [0x000cf] in <c0efaab9a8d94a42973f5bd70672a31f>:0 
--- End of stack trace from previous location where exception was thrown ---
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in <5fa4890be57f49d5a93452df59075ae8>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) [0x0003e] in <5fa4890be57f49d5a93452df59075ae8>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) [0x00028] in <5fa4890be57f49d5a93452df59075ae8>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) [0x00008] in <5fa4890be57f49d5a93452df59075ae8>:0 
  at System.Runtime.CompilerServices.TaskAwaiter`1[TResult].GetResult () [0x00000] in <5fa4890be57f49d5a93452df59075ae8>:0 
  at Zelda.BotW.Map.ViewModel.InAppPurchasingPageModel+h.d () [0x001eb] in <a72db26b0bbd432b883d921bd1fbbfe3>:0 

@jamesmontemagno
Copy link
Owner

hmmmm, i haven't changed any Android code lately to be honest with you. I just ran through this code in my sample app and had it working on android just fine:

 async Task LoadItemsAsync()
            {
                try
                {
                    IsBusy = true;
                    var connected = await CrossInAppBilling.Current.ConnectAsync();
                   
                    if(!connected)
                    {
                        await DependencyService.Get<IDialogs>().DisplayAlert("Can't connect", "Unable to connect to the app store, please check internet connection.");
                        return;
                    }

                    var purchases = await CrossInAppBilling.Current.GetPurchasesAsync(ItemType.InAppPurchase);

                    if (purchases.Count() > 0)
                        Settings.ShowAds = false;

                    var ids = purchases.Select(p => p.ProductId);

                    var items = await CrossInAppBilling.Current.GetProductInfoAsync(ItemType.InAppPurchase, "donate1", "donate2", "donate3");

                    Items.Clear();
                    foreach (var item in items)
                    {
                        Items.Add(new IAP
                        {
                            Product = item,
                            Purchased = ids.Contains(item.ProductId)
                        });
                    }
                }
                catch (InAppBillingPurchaseException purchaseEx)
                {
                    var message = string.Empty;
                    switch (purchaseEx.PurchaseError)
                    {
                        case PurchaseError.AppStoreUnavailable:
                            message = "Currently the app store seems to be unavailble. Try again later.";
                            break;
                        case PurchaseError.BillingUnavailable:
                            message = "Billing seems to be unavailable, please try again later.";
                            break;
                        case PurchaseError.PaymentInvalid:
                            message = "Payment seems to be invalid, please try again.";
                            break;
                        case PurchaseError.PaymentNotAllowed:
                            message = "Payment does not seem to be enabled/allowed, please try again.";
                            break;
                    }

                    if (string.IsNullOrWhiteSpace(message))
                        return;

                    await DependencyService.Get<IDialogs>().DisplayAlert("Purchase Problem", message);
                   
                }
                catch (Exception ex)
                {
                }
                finally
                {
                    await CrossInAppBilling.Current.DisconnectAsync();
                    IsBusy = false;
                }
            }
        }

The best way to diagnose this is to:

1.) Remove the plugin from your android project
2.) clone this repo (1-2 branch) and add in Abstractions, .Android, and Vending Library and add them as references to your Android project
3.) Make sure you sign your debug build with your keystore. such as: https://github.com/Azure-Samples/MyDriving/blob/master/src/MobileApps/MyDriving/MyDriving.Android/MyDriving.Android.csproj#L31-L35
4.) Make sure you version number and version id match exactly what is in the store.
5.) Install on a device that isn't logged in as a tester or your main dev account.

This will let you step through the code.

@Sebastian1989101
Copy link
Contributor Author

I've already tried a few things but can't get this error yet. I will try your suggestion, thanks for the response. I'm a bit confused that there is no exception from iOS logged yet. It has already installations on 17 devices refered to itunesconnect and the testers know that the restore needs to be tested.

@Sebastian1989101
Copy link
Contributor Author

Just found out where the exception comes from. It happens if you try to restore without any purchase on this account before. Maybe that was the same for iOS? I will monitor this in the next few days.

@jamesmontemagno
Copy link
Owner

Yeah, so that method will return null if there are no purchases.. I could change to a blank list, but I just check against null. I bet that was the issue. Let's hope my other fix for iOS works

@Sebastian1989101
Copy link
Contributor Author

So I finally git some results... this time the apple review process was a little bit longer... Here are all exception that don't say "Canceled by User", "Cannot connect to Store", "Unable to process purchase" or "Purchase canceled":

iOS - 2017-07-06 13:21:34, 2017-07-06 05:34:41 and 2017-07-06 05:34:32 - NullReferenceException

System.NullReferenceException: Object reference not set to an instance of an object
  at Plugin.InAppBilling.InAppBillingImplementation+<PurchaseAsync>d__11.MoveNext () <0x101431e30 + 0x001d0> in <e96da8711c6f40319ddcc0869d1a57a2#1391f55dbbc5e22c2fe6465366ce7c30>:0 
--- End of stack trace from previous location where exception was thrown ---
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () <0x1004bd500 + 0x00028> in <2f4074c3120b4d80802e10af84b67d41#1391f55dbbc5e22c2fe6465366ce7c30>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) <0x1004c0260 + 0x000d3> in <2f4074c3120b4d80802e10af84b67d41#1391f55dbbc5e22c2fe6465366ce7c30>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) <0x1004c01c0 + 0x0007f> in <2f4074c3120b4d80802e10af84b67d41#1391f55dbbc5e22c2fe6465366ce7c30>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) <0x1004c0160 + 0x00047> in <2f4074c3120b4d80802e10af84b67d41#1391f55dbbc5e22c2fe6465366ce7c30>:0 
  at System.Runtime.CompilerServices.TaskAwaiter`1[TResult].GetResult () <0x1004c04f0 + 0x0001b> in <2f4074c3120b4d80802e10af84b67d41#1391f55dbbc5e22c2fe6465366ce7c30>:0 
  at Zelda.BotW.Map.ViewModel.InAppPurchasingPageModel+<OnPurchaseProductCommandExecute>d__16.MoveNext () <0x1000ad6f0 + 0x007fb> in <2d55d5dbdff0443e921679e3c0514b99#1391f55dbbc5e22c2fe6465366ce7c30>:0 

iOS - 2017-07-06 07:40:35 and 2017-07-06 00:49:46 - Restore failed

Plugin.InAppBilling.Abstractions.InAppBillingPurchaseException: Restore Transactions Failed
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () <0x100499500 + 0x00028> in <2f4074c3120b4d80802e10af84b67d41#1391f55dbbc5e22c2fe6465366ce7c30>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) <0x10049c260 + 0x000d3> in <2f4074c3120b4d80802e10af84b67d41#1391f55dbbc5e22c2fe6465366ce7c30>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) <0x10049c1c0 + 0x0007f> in <2f4074c3120b4d80802e10af84b67d41#1391f55dbbc5e22c2fe6465366ce7c30>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) <0x10049c160 + 0x00047> in <2f4074c3120b4d80802e10af84b67d41#1391f55dbbc5e22c2fe6465366ce7c30>:0 
  at System.Runtime.CompilerServices.TaskAwaiter`1[TResult].GetResult () <0x10049c4f0 + 0x0001b> in <2f4074c3120b4d80802e10af84b67d41#1391f55dbbc5e22c2fe6465366ce7c30>:0 
  at Plugin.InAppBilling.InAppBillingImplementation+<GetPurchasesAsync>d__9.MoveNext () <0x10140d7f0 + 0x0012f> in <e96da8711c6f40319ddcc0869d1a57a2#1391f55dbbc5e22c2fe6465366ce7c30>:0 
--- End of stack trace from previous location where exception was thrown ---
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () <0x100499500 + 0x00028> in <2f4074c3120b4d80802e10af84b67d41#1391f55dbbc5e22c2fe6465366ce7c30>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) <0x10049c260 + 0x000d3> in <2f4074c3120b4d80802e10af84b67d41#1391f55dbbc5e22c2fe6465366ce7c30>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) <0x10049c1c0 + 0x0007f> in <2f4074c3120b4d80802e10af84b67d41#1391f55dbbc5e22c2fe6465366ce7c30>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) <0x10049c160 + 0x00047> in <2f4074c3120b4d80802e10af84b67d41#1391f55dbbc5e22c2fe6465366ce7c30>:0 
  at System.Runtime.CompilerServices.TaskAwaiter`1[TResult].GetResult () <0x10049c4f0 + 0x0001b> in <2f4074c3120b4d80802e10af84b67d41#1391f55dbbc5e22c2fe6465366ce7c30>:0 
  at Zelda.BotW.Map.ViewModel.InfoPageModel+<OnRestoreCommandExecute>d__27.MoveNext () <0x100090b20 + 0x00387> in <2d55d5dbdff0443e921679e3c0514b99#1391f55dbbc5e22c2fe6465366ce7c30>:0 
  

@jamesmontemagno
Copy link
Owner

So, it looks like:

  1. The null reference is from me somewhere.... that is really tricky and need to look at it. This is in PUrchaseAsync so I will add more null checks I guess.....
  2. This is stemming from the fact that the user is trying to restore purchases, but nothing to restore: https://github.com/jamesmontemagno/InAppBillingPlugin/blob/master/src/Plugin.InAppBilling.iOS/InAppBillingImplementation.cs#L116 In this instance I am thinking maybe I should return a blank list of items?

@Sebastian1989101
Copy link
Contributor Author

  1. Sounds great. I will test it when you updated your library. The next version of my app will be uploaded at the beginning of next week, at least if I got a working Mac until than. My 8 year old Mac died and the new ordered one comes defect yesterday and I have to wait for the replacement from Apple... But actual I'm not so confident that the replacement comes so soon but the order page says it (custom configured and last time I have to wait 3 weeks).
  2. Total forgot about this case. So I will ignore this exception in the future.

@jamesmontemagno
Copy link
Owner

jamesmontemagno commented Jul 6, 2017

It looks like for the Restore failed it used to throw just a generic exception, but 2 months ago we changed it to return a normal exception. Basically when you catch that exception you need to know if it really was an error because they purchased something before and it couldn't be restored or was it because they haven't purchased yet. Very tricky.

@Sebastian1989101
Copy link
Contributor Author

Just released the new version of my app (including 1.2.2 of you plugin) and still got some reports about this issue (I've shared ~50 codes per platform per IAP so there are now way more users that could have this problem). So here is the list of exceptions I get so far, maybe some of them help you to improve the plugin because I think this plugin is great (after I implemented it one time by myself I know how much trouble this is even if you just implement one platform as I did with Swift):

iOS - Restore - 2017-07-22 15:28:403 - this happens multiple times, so far I see 37 in my log from the 50 users who uses the codes - at least it looks like this is the only restore exception I got:

Plugin.InAppBilling.Abstractions.InAppBillingPurchaseException: Restore Transactions Failed
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () <0x1004a9500 + 0x00028> in <2f4074c3120b4d80802e10af84b67d41#ce9b7db618ce4b2a43a52493b4323821>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) <0x1004ac260 + 0x000d3> in <2f4074c3120b4d80802e10af84b67d41#ce9b7db618ce4b2a43a52493b4323821>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) <0x1004ac1c0 + 0x0007f> in <2f4074c3120b4d80802e10af84b67d41#ce9b7db618ce4b2a43a52493b4323821>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) <0x1004ac160 + 0x00047> in <2f4074c3120b4d80802e10af84b67d41#ce9b7db618ce4b2a43a52493b4323821>:0 
  at System.Runtime.CompilerServices.TaskAwaiter`1[TResult].GetResult () <0x1004ac4f0 + 0x0001b> in <2f4074c3120b4d80802e10af84b67d41#ce9b7db618ce4b2a43a52493b4323821>:0 
  at Plugin.InAppBilling.InAppBillingImplementation+<GetPurchasesAsync>d__9.MoveNext () <0x101422050 + 0x0012f> in <03b01e063b3148aa8db6cea4b40d9ab5#ce9b7db618ce4b2a43a52493b4323821>:0 
--- End of stack trace from previous location where exception was thrown ---
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () <0x1004a9500 + 0x00028> in <2f4074c3120b4d80802e10af84b67d41#ce9b7db618ce4b2a43a52493b4323821>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) <0x1004ac260 + 0x000d3> in <2f4074c3120b4d80802e10af84b67d41#ce9b7db618ce4b2a43a52493b4323821>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) <0x1004ac1c0 + 0x0007f> in <2f4074c3120b4d80802e10af84b67d41#ce9b7db618ce4b2a43a52493b4323821>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) <0x1004ac160 + 0x00047> in <2f4074c3120b4d80802e10af84b67d41#ce9b7db618ce4b2a43a52493b4323821>:0 
  at System.Runtime.CompilerServices.TaskAwaiter`1[TResult].GetResult () <0x1004ac4f0 + 0x0001b> in <2f4074c3120b4d80802e10af84b67d41#ce9b7db618ce4b2a43a52493b4323821>:0 
  at Zelda.BotW.Map.ViewModel.InAppPurchasingPageModel+<OnRestorePurchasesCommandExecute>d__17.MoveNext () <0x100072060 + 0x00573> in <7d665ea69bd444fcaf17d2b6066010c9#ce9b7db618ce4b2a43a52493b4323821>:0 

And then there are also this exception but I don't know how relevant they are:

Android - Purchase - 2017-07-22 15:45:14 - this one looks like a cancel from the user for me but it happens two times from the same user:

Plugin.InAppBilling.Abstractions.InAppBillingPurchaseException: Unable to process purchase.
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in <5cc07b54abad4535bf9225597dd89c18>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) [0x0003e] in <5cc07b54abad4535bf9225597dd89c18>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) [0x00028] in <5cc07b54abad4535bf9225597dd89c18>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) [0x00008] in <5cc07b54abad4535bf9225597dd89c18>:0 
  at System.Runtime.CompilerServices.TaskAwaiter`1[TResult].GetResult () [0x00000] in <5cc07b54abad4535bf9225597dd89c18>:0 
  at Plugin.InAppBilling.InAppBillingImplementation+<PurchaseAsync>d__27.MoveNext () [0x0023d] in <47ea5559ddee42bea2649c0955a29940>:0 
--- End of stack trace from previous location where exception was thrown ---
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in <5cc07b54abad4535bf9225597dd89c18>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) [0x0003e] in <5cc07b54abad4535bf9225597dd89c18>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) [0x00028] in <5cc07b54abad4535bf9225597dd89c18>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) [0x00008] in <5cc07b54abad4535bf9225597dd89c18>:0 
  at System.Runtime.CompilerServices.TaskAwaiter`1[TResult].GetResult () [0x00000] in <5cc07b54abad4535bf9225597dd89c18>:0 
  at Plugin.InAppBilling.InAppBillingImplementation+<PurchaseAsync>d__26.MoveNext () [0x000d5] in <47ea5559ddee42bea2649c0955a29940>:0 
--- End of stack trace from previous location where exception was thrown ---
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in <5cc07b54abad4535bf9225597dd89c18>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) [0x0003e] in <5cc07b54abad4535bf9225597dd89c18>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) [0x00028] in <5cc07b54abad4535bf9225597dd89c18>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) [0x00008] in <5cc07b54abad4535bf9225597dd89c18>:0 
  at System.Runtime.CompilerServices.TaskAwaiter`1[TResult].GetResult () [0x00000] in <5cc07b54abad4535bf9225597dd89c18>:0 
  at Zelda.BotW.Map.ViewModel.InfoPageModel+b.d () [0x0014b] in <8a99c5817a354a82bd6aa4424f02d89d>:0 

iOS - Purchase - 2017-07-22 - I guess this is the same as last time? So it might be irrelevant, happens two times for the same user:

System.NullReferenceException: Object reference not set to an instance of an object
  at Plugin.InAppBilling.InAppBillingImplementation+<PurchaseAsync>d__11.MoveNext () <0x101486690 + 0x001d0> in <03b01e063b3148aa8db6cea4b40d9ab5#ce9b7db618ce4b2a43a52493b4323821>:0 
--- End of stack trace from previous location where exception was thrown ---
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () <0x10050d500 + 0x00028> in <2f4074c3120b4d80802e10af84b67d41#ce9b7db618ce4b2a43a52493b4323821>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) <0x100510260 + 0x000d3> in <2f4074c3120b4d80802e10af84b67d41#ce9b7db618ce4b2a43a52493b4323821>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) <0x1005101c0 + 0x0007f> in <2f4074c3120b4d80802e10af84b67d41#ce9b7db618ce4b2a43a52493b4323821>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) <0x100510160 + 0x00047> in <2f4074c3120b4d80802e10af84b67d41#ce9b7db618ce4b2a43a52493b4323821>:0 
  at System.Runtime.CompilerServices.TaskAwaiter`1[TResult].GetResult () <0x1005104f0 + 0x0001b> in <2f4074c3120b4d80802e10af84b67d41#ce9b7db618ce4b2a43a52493b4323821>:0 
  at Zelda.BotW.Map.ViewModel.InAppPurchasingPageModel+<OnPurchaseProductCommandExecute>d__16.MoveNext () <0x1000d4150 + 0x007fb> in <7d665ea69bd444fcaf17d2b6066010c9#ce9b7db618ce4b2a43a52493b4323821>:0 

And thats it. All others are "Cannot connect to store"-erros.

@overzealus
Copy link

I'm having the same trouble with in-apps on iOS, and I can add some info that can bring some light to this problem:

  1. when user is trying to buy product, that he had already purchased, iOS says something like "Product already purchased". After that notification nothing happens. I tried to implement logging in a real app and I've found that awaiting of PurchaseAsync method never returns result.
    Xamarin docs saying something about original transaction inside transaction that you receive. May be there is the issue?
  2. I have received one user report that a real purchase (for money) didn't happen: he was charged, but nothing happened and user described the same situation as when promo codes are not working. So, this could happen to a real purchase too.

@Sebastian1989101
Copy link
Contributor Author

I can confirm that this also happens with real purchases. I got two reports about exactly this problem (it must be a never ending task because I see the "Purchase start" log entry but neither a failure or a success response).

@overzealus
Copy link

And it never happens in a sandbox or TestFlight.

@Sebastian1989101
Copy link
Contributor Author

I got more and more users who are reporting this issue but I cannot see anything is the log except that they started the purchase but it never goes into the "success" or "exception" state. Looks a bit like a deadlock in some cases for iOS?

@jamesmontemagno
Copy link
Owner

I am also reading through this for promo codes: https://stackoverflow.com/questions/41931584/ios-detecting-promo-code-in-app-purchases

What does your code snippet look like for making the purchase?

How about Android? Any issues there?

@Sebastian1989101
Copy link
Contributor Author

Android seems to have no problems at all. This is my code for the purchase (Forms / Shared):

if (!CrossInAppBilling.IsSupported)
    return;

ProcessIsRunning = true;
var billing = CrossInAppBilling.Current;

try
{
    if (obj.ID == PurchaseableProduct.ALL_PAGES)
    {
        if (Products.Any(p => p.Purchased && p.IsPageUnlock))
        {
            if (!await page.DisplayAlert("Security question", $"Are you sure, that you want to purchase \"{obj.Name}\"? You already purchased one or more pages separately.", "Yes", "No"))
                return;
        }
    }

    var connection = await billing.ConnectAsync();
    if (!connection)
    {
        await page.DisplayAlert("Error", "Error while connecting to Store.", "OK");
        return;
    }

    var purchase = await billing.PurchaseAsync(obj.ID, ItemType.InAppPurchase, "apppayload", new InAppBillingVerifyPurchase());
    if (purchase == null)
    {
        await LogPaymentError(null);
        await page.DisplayAlert("Error", "Error while purchasing product.", "OK");
    }
    else
    {
        context.Send(c => obj.Purchased = true, null);
        await LogPaymentSuccess(purchase);

        if (await MarkProductAsPurchased(obj.ID))
            await page.DisplayAlert("Status", "Product successfully purchased and activated.", "OK");
    }
}
catch (InAppBillingPurchaseException purchaseEx)
{
    var message = GetInAppBillingPurchaseExceptionMessage(purchaseEx);
    if (string.IsNullOrEmpty(message))
        return;

    await LogPaymentError(purchaseEx);

    await page.DisplayAlert("Error", message, "OK");
}
catch (Exception ex)
{
    await LogPaymentError(ex);
    await page.DisplayAlert("Error", ex.Message, "OK");
}
finally
{
    await billing.DisconnectAsync();

    CrossInAppBilling.Dispose();
    ProcessIsRunning = false;
}

@jamesmontemagno
Copy link
Owner

All looks relatively alright, I don't really ever dispose of mine though, i see no reason to disconnect the observer at all.

https://developer.apple.com/library/content/technotes/tn2387/_index.html is what I am reading and it is pretty much spot on to my code and implementation.

The only thing is that you should be able to query all of the purchases , which will also finish any pending transactions that are there.

Apple makes you implement a restore button so this should also do it.

@Sebastian1989101
Copy link
Contributor Author

A Restore button is also implemented. The dispose was added just to see if it makes any difference. I will check if the usage of SKPaymentQueue change something.

@overzealus
Copy link

Some more info.
In my app I have 3 in-app purchases.
I have used promo codes on my own device to test 2 of them and one have a bug, that is discussed here and the other one - doesn't.
I have completely removed this plugin and created my own code for in-app on iOS, using DependencyService and events.
Tried everything in sandbox and testflight - running OK.
Uploaded to appstore, tested on my device and the same trouble: first in-app purchase not working, second - OK.
So, now the positive info, that could give us some thoughts on what's wrong.
When I'm trying to purchase first product (not working), I have a messages with something like: "You have already purchased this product. Content would be restored without additional payment" and "OK" button.
When I'm trying to purchase second product, I receive another kind of message: "You have already purchased this product. Would you like to download it again for free?" and "OK" and "Cancel" buttons.
Both in-app products are non-consumable and don't have any downloadable content. In itunesconnect their properties are the same, but SKPaymentQueue gives me different messages when I'm trying to purchase them again.
Sad, that I can test it only in real app store :( I'll try to add some loging to see what's going on.
At the moment I can only assume, that SKPaymentTransactionObserver TransactionsUpdated is not raised, or... something gives a deadlock when FinishTransaction is called, because my code first calls FinishTransaction, than raises event, that provides purchase info to shared code.

@jamesmontemagno
Copy link
Owner

Ahhh that is interesting... if you gave out the promo code and it is trying to be purchased again then it could get stuck.... That is interesting.... but very strange.... maybe we need to check for a specific error code.

@Sebastian1989101
Copy link
Contributor Author

It gets stuck with regular purchases as well sometimes. The PurchaseAsync method is called but the callback never comes. Just got new reports in the last few days from people with this problem and I have not create new codes yet (also I see in my log the entry that I do before "PurchaseAsync" but neither the success or the exception log is triggered).

@overzealus
Copy link

overzealus commented Sep 7, 2017

OK, guys, now I know what's the problem.
First of all, we need to override not just TransactionsUpdated, but other methods of PaymentObserver too.
Second, the main trouble - sometimes transactions are in SKPaymentQueue, but not firing PaymentObserver events.
I don't know, if it's a Xamarin/Mono bug or Apple bug, but that's what I recommend: before starting restore, check for transactions in SKPaymentQueue.DefaultQueue.Transactions.
Without running a SKPaymentQueue.DefaultQueue.RestoreCompletedTransactions I can find there a bunch of transactions, some of them are in "purchased" state - and that's the missing transactions!
Also, I recommend you to inspect OriginalTransaction recursively to find transaction with purchased or restored state.

@Sebastian1989101
Copy link
Contributor Author

From the first two days testing this it seems to work what overzealus mentioned. Are there any plans to include it directly to the Plugin? Because it would be much cleaner if the IAP code is in one place.

@jamesmontemagno
Copy link
Owner

Yes, we should be able to, i am open to pull requests if this is fixing things up for everyone. Please help out :)

@jamesmontemagno
Copy link
Owner

I will need a PR from you guys who have found some solutions to help out. since i can't reproduce.

@Sebastian1989101
Copy link
Contributor Author

I guess the solution from overzealus would be better. I cannot test it on my own too and can only relate on the response of my users (and even if I trust most of them, I cannot confirm it 100% by myself).

@overzealus
Copy link

@jamesmontemagno, the solution is really simple:

  1. Add a recursive method to check if transaction has OriginalTransaction.
    As for me, when we find any non-consumable transaction with state "purchased", we can return result true.
    Code sample (not the best, I know):
    public static bool CheckIfTransactionPurchased(SKPaymentTransaction transaction) { if (transaction.TransactionState == SKPaymentTransactionState.Purchased) return true; else { if (transaction.OriginalTransaction != null) return CheckIfTransactionPurchased(transaction.OriginalTransaction); else return false; } }
  1. Before using SKPaymentQueue.DefaultQueue.RestoreCompletedTransactions(); check SKPaymentQueue.DefaultQueue.Transactions for transactions with purchased status.
    foreach (var trans in SKPaymentQueue.DefaultQueue.Transactions) { if (CheckIfTransactionPurchased(trans)) { //Found transaction, that was purchased, need to do something here, may be collect this transaction to a list } }

PS. I don't know, what should we do with consumables or subscriptions, since I don't use them, and I'm not sure, that bug happens with them and this code can give unpredictable results with consumables!

@timdmaxey
Copy link

Sebastian1989101 my issues is I can purchase my auto renew subscription get a purchaseID of 100003456789 as an example and the sandbox renews up to 5 times or whatever...

Then when I check to see if I purchased the subscription already using var purchases = await billing.GetPurchasesAsync(ItemType.Subscription); it seems I get the same purchaseID over and over again.

So I purchase again (after it stopped renewing) and I get a new purchaseID 10000345555 for instance, and then I update the server with the new purchaseID

I run the var purchases = await billing.GetPurchasesAsync(ItemType.Subscription); and I never get the new purchaseID, it's always the first one.

Is this correct? Should I not update the server code with the new purchaseID, just keep the original?

@Sebastian1989101
Copy link
Contributor Author

@jamesmontemagno Is there a chance that this getting fixed soon? I'm currently working on 3 new projects and fixing my old ones for iOS 11 / watchOS 4. Would be great to just use a updated plugin instead of inserting this workaround in each project.

@jamesmontemagno
Copy link
Owner

I am not sure exactly what the workaround is to be honest.... @Sebastian1989101 do you have a workaround?

I am looking at what @overzealus did, but literally looping through each of the items I don't understand that at all.

@Sebastian1989101
Copy link
Contributor Author

I'm using a pretty similar solution as @overzealus did. There are a few transactions sometimes in the queue that can be grabbed from there. It's possible that a transaction / a purchase gets a timeout (in this case the Plugin.InAppBilling never comes back from a Async call). Thats exactly what happend if someone had this bug where only the first purchase is restored.

@jamesmontemagno
Copy link
Owner

So do you add those transactions to the returned ones? I'll post a snippet with what I think should happen.

@overzealus
Copy link

@ jamesmontemagno the main solution is not just looping through transaction, but checking transaction queue (SKPaymentQueue.DefaultQueue.Transactions) before you call SKPaymentQueue.DefaultQueue.RestoreCompletedTransactions();
The problem is that for some unkown reasons, some transactions appears in queue, but they don't fire events (methods) on PaymentObserver (regarding Apple documentation - they should do it, but they don't).

And about looping inside transactions. Each transaction have a property of the same type, called OriginalTransaction. It's a transaction, that happened before and connected with the inspectable transation.
Sometimes, each original transaction can have another originaltransaction. And sometimes they can have such structure:
-transaction 1: Failed
--transaction 2: Restored
---transaction 3: Purchased

So, if we check status of the one, that is on the "top" - we'll have failed status (we don't know the reason, may be user had some internet connection problems). But, if we check inner transaction, we can see, that user have already purchased it and restored it (may be on previous installation of the app).

@jamesmontemagno
Copy link
Owner

So.... would it be something like this:


		Task<SKPaymentTransaction[]> RestoreAsync()
		{
			var tcsTransaction = new TaskCompletionSource<SKPaymentTransaction[]>();

			var allTransactions = new List<SKPaymentTransaction>();

			Action<SKPaymentTransaction[]> handler = null;
			handler = new Action<SKPaymentTransaction[]>(transactions =>
			{

				// Unsubscribe from future events
				paymentObserver.TransactionsRestored -= handler;

				if (transactions == null)
				{
					if (allTransactions.Count == 0)
						tcsTransaction.TrySetException(new InAppBillingPurchaseException(PurchaseError.RestoreFailed, "Restore Transactions Failed"));
					else
						tcsTransaction.TrySetResult(allTransactions.ToArray());
				}
				else
				{
					allTransactions.AddRange(transactions);
					tcsTransaction.TrySetResult(allTransactions.ToArray());
				}
			});

			paymentObserver.TransactionsRestored += handler;

			foreach (var trans in SKPaymentQueue.DefaultQueue.Transactions)
			{
				var original = FindOriginalTransaction(trans);
				if (original == null)
					continue;

				allTransactions.Add(original);
			}

			// Start receiving restored transactions
			SKPaymentQueue.DefaultQueue.RestoreCompletedTransactions();

			return tcsTransaction.Task;
		}



		static SKPaymentTransaction FindOriginalTransaction(SKPaymentTransaction transaction)
		{
			if (transaction == null)
				return null;

			if (transaction.TransactionState == SKPaymentTransactionState.Purchased)
				return transaction;

			if (transaction.OriginalTransaction != null)
				return FindOriginalTransaction(transaction.OriginalTransaction);

			return transaction;

		}

@Sebastian1989101
Copy link
Contributor Author

Sebastian1989101 commented Oct 8, 2017

I guess sometimes the Payment also stays in Pending status for some reason. But maybe @overzealus could be more specific than me.

Edit: Can you maybe roll out a beta version with this changes? So that we can test it if it has the same result as the custom implementation.

@desunit
Copy link

desunit commented Oct 9, 2017

I receive more and more complains from users about the problem too. @jamesmontemagno, the implementation of suggested solution from @overzealus looks good to me and I'll be happy to test it with the next update. Any plans to release the update soon?

@overzealus Do we need to use FindOriginalTransaction for Purchase method too?

@ghost ghost removed the triaged label Oct 9, 2017
@jamesmontemagno
Copy link
Owner

Check: 1.2.3.98-beta

@desunit
Copy link

desunit commented Oct 10, 2017

Just received feedback from users - the new update works perfectly. Thanks so much!

@Sebastian1989101
Copy link
Contributor Author

Sebastian1989101 commented Oct 10, 2017

Got the exact same feedback as @desunit from my TestFlight users so far. I hope it works as well on the release version it is currently in review by apple.

@jamesmontemagno
Copy link
Owner

yay! i am glad we were able to all work together on this one. I wish that Apple's API was better :) <3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants