Forum Xamarin.iOS

Banging my head against the wall implementing IAP on iOS: 'Cannot connect to iTunes Store'. Help :/

I've asked this question in the /Forms forum, but got no response. I guess it was the wrong place to ask since it is an iOS problem. I'll try again here, and see if anyone can give me pointers.

I've used InAppBillingPlugin (https://github.com/jamesmontemagno/InAppBillingPlugin) to implemented IAP in my app. I have it working on Android, but I can't seem to make it work on iOS. I keep getting the 'Cannot connect to iTunes Store' error when trying to purchase an item. I can request current purchases however, since I can't make purchases it returns an empty list, as expected.

The exact same code work on Android. I've been add it for over a week now, and have nothing to show for it. I've tried everything that I could find: multiple sandbox users tried, reset iPhone, clean caches, different real users, multiple devices, checked build number, delete app of device, ...

When I'm fully logged out on the device, and I run my app it does ask for the users login & password. The code for connecting to the store also works. It is when calling the purchase method that it comes back with an exception.
My IAP items have been submitted and approved by Apple.
I had a com.company.* provisioning profile, and have changed this to a non-wildcard profile

I have no further error message, or any idea where to start looking for them.
Does anyone have any experience with the plugin? Or with IAP on iOS since I think it isn't because of the PlugIn.

Answers

  • SebastianKruseSebastianKruse USMember ✭✭✭
    edited April 2017

    @TommyUytterhaegen I'm using this plugin in a few projects successfully already and all the problem I got is on Android (at least on my first two implementations... now I know how it works and have no problem with it at all). So what exactly do you need? First of all, the IAP does not work on the iOS Emulator - you need real devices. Also the plugin has problems with debug but works very well on TestFlight (at least until you cancel a purchase) but on the release AppStore version it has absolutly no problem at all.

    First of all you need to add the IAP in iTunesConnect / Google Developer Console. In best case use the same Product ID across the platforms.

    Than you need to add the NuGet package in your project. The only ones I used so far is 1.1.0.24-beta and 1.1.0.34-beta.

    Only on Android you have to add this to your main activity:
    protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
    {
    base.OnActivityResult(requestCode, resultCode, data);
    InAppBillingImplementation.HandleActivityResult(requestCode, resultCode, data);
    }

    Nest step is to add the IInAppBillingVerifyPurchase implementation.
    public class InAppBillingVerifyPurchase : IInAppBillingVerifyPurchase
    {
    const string key1 = @use your android key here;
    const string key2 = @use your android key here;
    const string key3 = @use your android key here;

                public Task<bool> VerifyPurchase(string signedData, string signature, string productId = null, string transactionId = null)
                {
        #if __ANDROID__
                    var key1Transform = Plugin.InAppBilling.InAppBillingImplementation.InAppBillingSecurity.TransformString(key1, 1);
                    var key2Transform = Plugin.InAppBilling.InAppBillingImplementation.InAppBillingSecurity.TransformString(key2, 2);
                    var key3Transform = Plugin.InAppBilling.InAppBillingImplementation.InAppBillingSecurity.TransformString(key3, 3);
    
                    return Task.FromResult(Plugin.InAppBilling.InAppBillingImplementation.InAppBillingSecurity.VerifyPurchase(key1Transform + key2Transform + key3Transform, signedData, signature));
        #else
                    return Task.FromResult(true);
        #endif
                }
            }
    

    For the next step I add a local DTO to handle the products a little bit better. But that might be a personal thing.
    public class PurchaseableProduct : INotifyPropertyChanged, IComparable
    {
    private bool _purchased;

            public const string RECIPES = "com.mycomp.myapp.recipes";
            public const string COMPENDIUM = "com.mycomp.myapp.compendium";
            public const string ARMOR = "com.mycomp.myapp.armor";
            public const string ALL_PAGES = "com.mycomp.myapp.allpages";
            public const string REMOVE_ADS = "com.mycomp.myapp.removeads";
    
            public string ID { get; }
            public string ShortID => ID?.Split(new[] { '.', '-', '_', ' ' }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault();
    
            public int IDValue //Just for sorting my IAP's
            {
                get
                {
                    switch (ID)
                    {
                        case RECIPES: return 1;
                        case COMPENDIUM: return 2;
                        case ARMOR: return 3;
                        case ALL_PAGES: return 99;
                        case REMOVE_ADS: return 100;
                        default: return 0;
                    }
                }
            }
    
            public bool IsPageUnlock => ID == RECIPES || ID == COMPENDIUM || ID == ARMOR || ID == ALL_PAGES;
    
            public string Name { get; }
            public string Description { get; }
    
            public string Price { get; }
    
            public bool NotPurchased => !Purchased;
            public bool Purchased
            {
                get { return _purchased; }
                set
                {
                    _purchased = value;
    
                    OnPropertyChanged();
                    OnPropertyChanged(nameof(NotPurchased));
                }
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            public PurchaseableProduct(string id, string name, string description, string price)
            {
                ID = id;
    
                Name = name;
                Description = description;
    
                Price = price;
            }
    
            protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
            {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
    
            public int CompareTo(PurchaseableProduct other)
            {
                return IDValue.CompareTo(other.IDValue);
            }
        }
    
    And finally the implementation of your ViewModel. 
            public class InAppPurchasingPageModel : ViewModelBase
            {
                private bool _processIsRunning;
    
                private readonly string[] _products =
                    {
                        PurchaseableProduct.RECIPES,
                        PurchaseableProduct.COMPENDIUM,
                        PurchaseableProduct.ARMOR,
                        PurchaseableProduct.ALL_PAGES,
                        PurchaseableProduct.REMOVE_ADS
                    };
    
                public ObservableCollection<PurchaseableProduct> Products { get; }
    
                public bool ProcessIsRunning
                {
                    get { return _processIsRunning; }
                    set { _processIsRunning = value; OnPropertyChanged(); }
                }
    
                public Command<PurchaseableProduct> PurchaseProductCommand { get; }
                public Command RestorePurchasesCommand { get; }
    
                public InAppPurchasingPageModel(Page page)
                    : base(page)
                {
                    Products = new ObservableCollection<PurchaseableProduct>();
    
                    PurchaseProductCommand = new Command<PurchaseableProduct>(OnPurchaseProductCommandExecute);
                    RestorePurchasesCommand = new Command(OnRestorePurchasesCommandExecute);
    
                    LoadProducts();
                }
    
                private async void LoadProducts()
                {
                    try
                    {
                        ProcessIsRunning = true;
    
                        var connection = await CrossInAppBilling.Current.ConnectAsync();
                        if (!connection)
                        {
                            await page.DisplayAlert("Error", "Error while connecting to Store.", "OK");
                            return;
                        }
    
                        var inAppBillingProducts = (await CrossInAppBilling.Current.GetProductInfoAsync(ItemType.InAppPurchase, _products))?.ToArray();
                        if (inAppBillingProducts != null && inAppBillingProducts.Any())
                        {
                            var products = inAppBillingProducts.Select(product => new PurchaseableProduct(product.ProductId, product.Name, product.Description, product.LocalizedPrice)).ToList();
                            products.Sort();
    
                            context.Send(obj => Products.Clear(), null);
                            foreach (var product in products)
                                context.Send(obj => Products.Add(product), null);
    
                            var appSettings = StorageManager.Instance.GetSingleEntry<AppSettings>();
                            if (appSettings.RemoveAds && Products.Any(p => p.ID == PurchaseableProduct.REMOVE_ADS))
                                Products.First(p => p.ID == PurchaseableProduct.REMOVE_ADS).Purchased = true;
    
                            if ((appSettings.UnlockRecipes || appSettings.UnlockAllPages) && Products.Any(p => p.ID == PurchaseableProduct.RECIPES))
                                Products.First(p => p.ID == PurchaseableProduct.RECIPES).Purchased = true;
    
                            if ((appSettings.UnlockArmor || appSettings.UnlockAllPages) && Products.Any(p => p.ID == PurchaseableProduct.ARMOR))
                                Products.First(p => p.ID == PurchaseableProduct.ARMOR).Purchased = true;
    
                            if ((appSettings.UnlockCompendium || appSettings.UnlockAllPages) && Products.Any(p => p.ID == PurchaseableProduct.COMPENDIUM))
                                Products.First(p => p.ID == PurchaseableProduct.COMPENDIUM).Purchased = true;
    
                            if (appSettings.UnlockAllPages && Products.Any(p => p.ID == PurchaseableProduct.ALL_PAGES))
                                Products.First(p => p.ID == PurchaseableProduct.ALL_PAGES).Purchased = true;
                        }
                    }
                    catch (Exception ex)
                    {
                        await page.DisplayAlert("Error", ex.Message, "OK");
                    }
                    finally
                    {
                        await CrossInAppBilling.Current.DisconnectAsync();
                        ProcessIsRunning = false;
                    }
                }
    
                private async void OnPurchaseProductCommandExecute(PurchaseableProduct obj)
                {
                    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;
                            }
                        }
    
                        ProcessIsRunning = true;
    
                        var connection = await CrossInAppBilling.Current.ConnectAsync();
                        if (!connection)
                        {
                            await page.DisplayAlert("Error", "Error while connecting to Store.", "OK");
                            return;
                        }
    
                        var purchase = await CrossInAppBilling.Current.PurchaseAsync(obj.ID, ItemType.InAppPurchase, "apppayload", new InAppBillingVerifyPurchase());
                        if (purchase == null)
                        {
                            await page.DisplayAlert("Error", "Error while purchasing product.", "OK");
                        }
                        else
                        {
                            context.Send(c => obj.Purchased = true, null);
                            if (await MarkProductAsPurchased(obj.ID))
                                await page.DisplayAlert("Status", "Product successfully purchased and activated.", "OK");
                        }
                    }
                    catch (Exception ex)
                    {
                        await page.DisplayAlert("Error", ex.Message, "OK");
                    }
                    finally
                    {
                        await CrossInAppBilling.Current.DisconnectAsync();
                        ProcessIsRunning = false;
                    }
                }
    
                private async void OnRestorePurchasesCommandExecute()
                {
                    try
                    {
                        ProcessIsRunning = true;
                        var productsRestored = false;
    
                        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)
                                            {
                                                Products.First(p => p.ID == purchase.ProductId).Purchased = true;
                                                if (await MarkProductAsPurchased(purchase.ProductId))
                                                    productsRestored = true;
                                            }
                                        }, null);
                                }
    
                                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 page.DisplayAlert("Status", productsRestored ? "Products have been successfully restored." : "Products could not be restored.", "OK");
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        await page.DisplayAlert("Error", ex.Message, "OK");
                    }
                    finally
                    {
                        await CrossInAppBilling.Current.DisconnectAsync();
                        ProcessIsRunning = false;
                    }
                }
    
                private async Task<bool> MarkProductAsPurchased(string productID)
                {
                    var appSettings = StorageManager.Instance.GetSingleEntry<AppSettings>();
                    var status = false;
    
                    switch (productID)
                    {
                        case PurchaseableProduct.RECIPES:
                            status = appSettings.UnlockRecipes = true;
                            break;
    
                        case PurchaseableProduct.ARMOR:
                            status = appSettings.UnlockArmor = true;
                            break;
    
                        case PurchaseableProduct.COMPENDIUM:
                            status = appSettings.UnlockCompendium = true;
                            break;
    
                        case PurchaseableProduct.ALL_PAGES:
                            status = appSettings.UnlockAllPages = true;
                            break;
    
                        case PurchaseableProduct.REMOVE_ADS:
                            status = appSettings.RemoveAds = true;
                            StaticAppEvents.InvokeRemoveAds(this);
                            break;
    
                        default:
                            await page.DisplayAlert("Error", $"Unable to restore product: {productID}", "OK");
                            break;
                    }
    
                    StorageManager.Instance.StoreSingleEntry(appSettings);
                    StaticAppEvents.InvokeRebuildMenuStructure(this);
    
                    return status;
                }
            }
    

    I hope my code is not too confusing and helps you with your problem. It is from one of my actual apps except the IAP ID's and the private Key.

  • TommyUytterhaegenTommyUytterhaegen BEMember ✭✭
    edited April 2017

    Thank you for sharing that code, very interesting! I'm running bare bone code right now to try to track this issue down.

    I haven't tried with TestFlight, but with a debug version. As I understand, this could be my problem? I would expect Apple to give 'sorry, debug enabled', or something as a problem back. I'll give this a try. I've never used it before, so I'll look into that one. I'll let you know how it goes.

    Extra question: Do you also use it for UWP IAP? If so, how did that go, still need to look into that :)

  • TommyUytterhaegenTommyUytterhaegen BEMember ✭✭
    edited April 2017

    @SebastianKruse
    My purchase code was pretty similar to your code, I updated it a bit since I won't be able to use a debugger when using TestFlight, so I write out my error messages :)

    Apple was fast and I was already able to use it in TestFlight and no-go: same problem, same error 'Cannot connect to iTunes Store' :(

    The connect part of the code works (ConnectAsync()), it is the buy part (PurchaseAsync()) that throws the error. It does however ask for the password (did that in debug as well), then a couple of seconds pass, and I get the error.

    Any ideas?

  • NicholasBauerNicholasBauer USMember ✭✭

    @TommyUytterhaegen - did you ever get your answer? Same problem here.

  • No. I still don't have IAP working in iOS :(

  • TonyGodtTonyGodt DKMember ✭✭

    To test IAP on iOS - you MUST test it on a real device!

    You can still use the "sandbox" user.

  • I've used real devices, multiple ones, with multiple accounts. Thanks for the suggestion though

  • ZoliZoli NLMember ✭✭✭
    edited March 2018

    Same issue I have, losing all my hair: code works prefect on Android, but usually(!) fails on iOS.
    I did everything properly, test on a device (iphone8 / iOS 11.0.2) with sandbox user, IAP item is in the iTunes, banking/tax/contact filled, etc.
    What is happening, I have a test user who bought it. In my app I want to restore the purchase after a new install, I call:

       var purchases = await billing.GetPurchasesAsync(ItemType.InAppPurchase);
    

    10%, it succeeds, 90% fails.
    If fails, not immediately, but after waiting about 30-40seconds(!!)
    If fails, this is the exception:

       [0:]   WasItemPurchased InAppBillingPurchaseException --->
     Plugin.InAppBilling.Abstractions.InAppBillingPurchaseException: Restore Transactions Failed
      at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.6.1.4/src/mono/mcs/class/referencesource/mscorlib/system/runtime/exceptionservices/exceptionservicescommon.cs:152 
      at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) [0x00037] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.6.1.4/src/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:187 
      at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) [0x00028] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.6.1.4/src/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:156 
      at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) [0x00008] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.6.1.4/src/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:128 
      at System.Runtime.CompilerServices.TaskAwaiter`1[TResult].GetResult () [0x00000] in :0 
      at Plugin.InAppBilling.InAppBillingImplementation+d__13.MoveNext () [0x00025] in C:\projects\inappbillingplugin\src\Plugin.InAppBilling.iOS\InAppBillingImplementation.cs:95 
    --- End of stack trace from previous location where exception was thrown ---
      at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.6.1.4/src/mono/mcs/class/referencesource/mscorlib/system/runtime/exceptionservices/exceptionservicescommon.cs:152 
      at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) [0x00037] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.6.1.4/src/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:187 
      at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) [0x00028] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.6.1.4/src/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:156 
      at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) [0x00008] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.6.1.4/src/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:128 
      at System.Runtime.CompilerServices.TaskAwaiter`1[TResult].GetResult () [0x00000] in :0 
    at MYAPP.IAP.IAP+d__10.MoveNext () [0x001b8] in E:\Project\Xamarin\MYAPP\IAP\IAP.cs:135 
    

    If succeeds (about 10%), GetPurchasesAsync() returns my purchase properly. But here also I have to wait ~30 seconds.
    Also, only 70% it asks for sandbox tester appleid/pwd (whilt it should ALWAYS), 30% does not.
    Dialog for appleid/pwd also sometimes pops up in 1 second, other times after ~30 seconds.

    Interstingly, GetProductInfoAsync() works 100% fine.

    Purchase is about the same, 90% fails (after about 30 seconds waiting), 10% succeeds:

    If fails:

    [0:] PurchaseItem Error -> Plugin.InAppBilling.Abstractions.InAppBillingPurchaseException: Cannot connect to iTunes Store
      at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.6.1.4/src/mono/mcs/class/referencesource/mscorlib/system/runtime/exceptionservices/exceptionservicescommon.cs:152 
      at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) [0x00037] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.6.1.4/src/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:187 
      at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) [0x00028] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.6.1.4/src/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:156 
      at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) [0x00008] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.6.1.4/src/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:128 
      at System.Runtime.CompilerServices.TaskAwaiter`1[TResult].GetResult () [0x00000] in :0 
      at Plugin.InAppBilling.InAppBillingImplementation+d__16.MoveNext () [0x0002b] in C:\projects\inappbillingplugin\src\Plugin.InAppBilling.iOS\InAppBillingImplementation.cs:187 
    --- End of stack trace from previous location where exception was thrown ---
      at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.6.1.4/src/mono/mcs/class/referencesource/mscorlib/system/runtime/exceptionservices/exceptionservicescommon.cs:152 
      at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) [0x00037] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.6.1.4/src/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:187 
      at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) [0x00028] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.6.1.4/src/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:156 
      at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) [0x00008] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.6.1.4/src/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:128 
      at System.Runtime.CompilerServices.TaskAwaiter`1[TResult].GetResult () [0x00000] in :0 
      at MYAPP.IAP.IAP+d__19.MoveNext () [0x00163] in E:\Project\Xamarin\MYAPP>\IAP\IAP.cs:328 
    
    

    Really out of idea to try, sent for Apple Store review 5 times (maybe something in my system), but NOT, they reported the same error.
    What is horrible, this inconsistency, I have not idea what is going on.
    Other funny thing, if I call GetProductInfoAsync() before GetPurchasesAsync(), then GetPurchasesAsync() tends to work more times (but not always, inconsistent)

    InAppBillingPlugin is the latest, 1.2.4

  • eeenmachineeeenmachine Member

    Anyone figure this out? I'm in the exact same spot. Works fine on Android, no dice on iOS, Testflight or otherwise. GetProductInfo works fine, purchases throw Cannot connect to iTunes store.

Sign In or Register to comment.