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

Windows Purchase Hangs #581

Open
brightertools opened this issue Dec 21, 2023 · 11 comments
Open

Windows Purchase Hangs #581

brightertools opened this issue Dec 21, 2023 · 11 comments

Comments

@brightertools
Copy link

brightertools commented Dec 21, 2023

I have implemented the In-App Billing, following the My Stream Timer example, the app is added to the store as well as the Add-On

When Initiating the Upgrade (purchase) My Laptop just tried to do face recognition without anything popping up, and got stuck doing that. I disabled the face recognition, which stops that but still hangs at that point?

Has anyone seen this issue?

In the My Stream Timer example, I see some kind of SetScreenSaver call that doesn't seem to do anything, I haven't implemented that, and assume its does nothing, but might have been something to try and mitigate some issue?

Code is very similar to the My Stream Timer Example

Note: I have downloaded the MyStreamTimer and that does popup the login (finger print verification, now that I have disabled the face recognition) The code is very similar (see below) but appreciate the My Stream Timer is not MAUI, specifically)

[RelayCommand]
private async Task UpgradeToFullVersion()
{
    if (IsBusyPurchasing)
    {
        return;
    }

    IsBusyPurchasing = true;

    try
    {
        NetworkAccess accessType = Connectivity.Current.NetworkAccess;

        if (accessType != NetworkAccess.Internet)
        {
            await DisplayAlert("No Internet", "Please check your internet connection and try again.");
            return;
        }

        var connected = await CrossInAppBilling.Current.ConnectAsync();

        if (!connected)
        {
            await DisplayAlert("App Store Connecting Error", "There was error connecting to the app store, check your internet connectivity and try again.");
            return;
        }

        var purchase = await CrossInAppBilling.Current.PurchaseAsync(FullVersionProductId, ItemType.InAppPurchase);

        if (purchase == null)
        {
            return;
        }

        if (purchase.State == PurchaseState.Purchased)
        {
            IsPurchased = true;

            SavePurchasedState();

            return;
        }

        throw new InAppBillingPurchaseException(PurchaseError.GeneralError);

    }
    catch (InAppBillingPurchaseException purchaseEx)
    {
        var message = string.Empty;
        switch (purchaseEx.PurchaseError)
        {
            case PurchaseError.AppStoreUnavailable:
                message = "The app store is currently unavailble, please try again later.";
                break;
            case PurchaseError.BillingUnavailable:
                message = "Billing is currently 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;
            case PurchaseError.UserCancelled:
                break;
            default:
                message = "Something has gone wrong, please try again.";
                break;
        }

        if (string.IsNullOrWhiteSpace(message))
        {
            return;
        }

        await DisplayAlert("Error Upgrading", message);
    }
    catch (Exception exception)
    {
        Console.WriteLine("Error Upgrading : " + exception.Message);
        await DisplayAlert("Error Upgrading", $"There was an issue upgrading, please try again. Error/Code: {exception.Message}");
    }
    finally
    {
        await CrossInAppBilling.Current.DisconnectAsync();

        if (IsPurchased)
        {
            PurchaseVersionLabel = "Full Version";
        }

        IsBusyPurchasing = false;
    }
}

Latest VS 2022, Latest MAUI (.net8) Latest InAppBillingPlugin, Windows 11 (Latest update)

App is available at this direct link if interested: https://www.microsoft.com/store/apps/9NVZFCB6NW1Q
Happy to share some voucher codes if anyone wants to test this, and might find this app useful (might have to DM me if possible)

Feedback (and clarification)

It seems there are few fundamental options to purchasing apps (from a windows perspective)

  1. Paid Version (with limited trial)

It seems that a paid version can be setup (with or without a trial) where the user pays for the app in the store (ie not an in-app purchase) and seems this is not supported by the InAppBillingPlugin and is implemented a different way (is this correct?)
eg: https://learn.microsoft.com/en-us/windows/uwp/monetize/get-license-info-for-apps-and-add-ons

It would seem that this option is simple but doesn't lend itself well to "Upgrades" as users need to go back to the store to purchase and is not that seamless.

  1. In-app Purchases (and subscriptions)

This is the purpose of the InAppBillingPlugin, where the purchase flow is all about a 'free' version (free in the store), plus the ability to purchase upgrades.

This flow naturally provides a way to offer upgrades within the app and get the user to upgrade directly.

  1. Hybrid of the above that could be confusing to users.

Is the above correct, it might be worth mentioning this in the docs.

@brightertools
Copy link
Author

brightertools commented Dec 21, 2023

One thing I have just thought of is that I am using MVVM and maybe need to run this code on the main thread, ie using InvokeOnMainThreadAsync will try that and report back.
UPDATE:
Makes no difference

UPDATE 2:

Setting Testmode

ie:

#if DEBUG
        CrossInAppBilling.Current.InTestingMode = true;
#endif

produces this exception:

System.ArgumentException: 'Value does not fall within the expected range.'

When calling

var purchase = await CrossInAppBilling.Current.PurchaseAsync(FullVersionProductId, ItemType.InAppPurchase);

@jamesmontemagno
Copy link
Owner

Code looks correct to me....

I honestly may just suggest throwing the code in there yourself as the Windows code is so minimal and see if you can debug through it:

https://github.com/jamesmontemagno/InAppBillingPlugin/blob/master/src/Plugin.InAppBilling/InAppBilling.uwp.cs

without seeing the full code I don't see any exceptions or crashes when i run the app. it is as if the code isn't being called... IsBusyPurchasing is that set correct?

@brightertools
Copy link
Author

Code looks correct to me....

I honestly may just suggest throwing the code in there yourself ....

Isolating that code (see below...) I get the same error (also below) unfortunately (in test mode) otherwise hangs.

If this doesn't work directly, then I am at a bit of a loss on any next steps..
CurrentApp.RequestProductPurchaseAsync() has 3 overloads, not sure if there are any new changes there?

Another person on another computer (latest windows 10) says the purchase didn't work but the restore did work after doing the purchase via the voucher code link (but the restore only worked when running the app as an administrator)
Running as an administrator seems like an odd requirement , any ideas on that?

My tests have only been trying to do this via the published app from the store (where I have no logs etc)
or
through Visual Studio (running as as admin, trying test mode or not)

It seems that once the app is installed via the app store that when run via Visual studio, its seems to replace the "deployed" version.. does this mean its effectively deployed as a published one? ie as if deployed via the app store and would behave the same? do you have any info/comment on that?

Any other ideas?

error:
`
{"Value does not fall within the expected range."}
Data: {System.Collections.ListDictionaryInternal}
HResult: -2147024809
HelpLink: null
InnerException: null
Message: "Value does not fall within the expected range."
ParamName: null
Source: "WinRT.Runtime"
StackTrace: " at WinRT.ExceptionHelpers.g__Throw|39_0(Int32 hr)\r\n at ABI.Windows.ApplicationModel.Store.ICurrentAppSimulatorWithConsumablesMethods.RequestProductPurchaseAsync(IObjectReference _obj, String productId, String offerId, ProductPurchaseDisplayProperties displayProperties)\r\n at Windows.ApplicationModel.Store.CurrentAppSimulator.RequestProductPurchaseAsync(String productId, String offerId, ProductPurchaseDisplayProperties displayProperties)\r\n at SocialTextEditor.Platforms.Windows.WindowsInAppBilling.d__2.MoveNext() in C:\Development\SocialTextEditor\SocialTextEditor\Platforms\Windows\WindowsInAppBilling.cs:line 27"
TargetSite: {Void g__Throw|39_0(Int32)}

`

`
using Windows.ApplicationModel.Store;

namespace SocialTextEditor.Platforms.Windows;

public static partial class WindowsInAppBilling
{

/// <summary>
/// Purchase specified product Id, return true if purchased
/// </summary>
/// <param name="productId"></param>
/// <returns></returns>
public static async Task<InAppBillingPurchase?> PurchaseAsync(string productId, bool testMode = false)
{
    // extracted:

    // purchaseResult = await CurrentAppMock.RequestProductPurchaseAsync(InTestingMode, productId);
    // --> var purchaseResult = await CurrentApp.RequestProductPurchaseAsync(FullVersionProductId);

    // to:

    try
    {

       
    var purchaseResult = testMode ? await CurrentAppSimulator.RequestProductPurchaseAsync(productId) : await CurrentApp.RequestProductPurchaseAsync(productId); ;

    if (purchaseResult == null)
    {
        return null;
    }

    if (string.IsNullOrWhiteSpace(purchaseResult.ReceiptXml))
    {
        return null;
    }

    return purchaseResult.ReceiptXml.ToInAppBillingPurchase(purchaseResult.Status).FirstOrDefault();
    }
    catch (Exception exception)
    {

        return null;
    }
}

}
`

@jamesmontemagno
Copy link
Owner

Hmmm that is odd.

My suggestion may be to use the new API that lives in Windows.Services.Store.... https://learn.microsoft.com/en-us/windows/uwp/monetize/in-app-purchases-and-trials I have been try to migrate the code slowly over to that.

https://learn.microsoft.com/en-us/windows/uwp/monetize/enable-in-app-purchases-of-apps-and-add-ons

The code is pretty straightforward.

@brightertools
Copy link
Author

brightertools commented Dec 24, 2023

My suggestion may be to use the new API that lives in Windows.Services.Store.... https://learn.microsoft.com/en-us/windows/uwp/monetize/in-app-purchases-and-trials I have been try to migrate the code slowly over to that.

I have looked at that example and isolated that purchase code, and now have this working to popup the login / purchase window..

Notes:

  1. This is specifically for in-app purchases with a productId (InAppOfferToken)
  2. I initially got an "invalid window handle" exception, so needed to create a window handle for app in MauiProgram.cs
  3. Not fully tested.
  4. Merry Christmas.

Purchase Code:

using Windows.Services.Store;

namespace SocialTextEditor.Platforms.Windows;

public static partial class WindowsInAppBilling
{
    public static async Task<bool> PurchaseAsync(string productId)
    {
        StoreContext storeContext = StoreContext.GetDefault();
        
        // This is required to prevent invalid window handle exception
        WinRT.Interop.InitializeWithWindow.Initialize(storeContext, MauiProgram.WindowHandle);
        
        try
        {
            string[] filterList = new string[] { "Consumable", "Durable", "UnmanagedConsumable" };

            // Get the list of purchase options related to this app
            StoreProductQueryResult productQueryResult = await storeContext.GetAssociatedStoreProductsAsync(filterList);

            if (productQueryResult.ExtendedError != null)
            {
                throw new Exception("There was an error getting product options for this application");
            }

            if (productQueryResult.Products.Count <= 0)
            {
                throw new Exception("No configured purchase options found for this application");
            }

            var product = productQueryResult.Products.Values.FirstOrDefault(x => x.InAppOfferToken == productId);

            if (product == null)
            {
                throw new Exception("Purchase option not found for this application");
            }

            StorePurchaseResult purchaseResult = await storeContext.RequestPurchaseAsync(product.StoreId);

            if (purchaseResult.ExtendedError != null)
            {
                throw new Exception("There was an error purchasing the application");
            }

            switch (purchaseResult.Status)
            {
                case StorePurchaseStatus.AlreadyPurchased:
                    {
                        throw new Exception("The application has already been purchased, please try restore app purchases");
                    }
                case StorePurchaseStatus.Succeeded:
                    {
                        return true;
                    }
                case StorePurchaseStatus.NotPurchased:
                    {
                        throw new Exception("Purchase unsucessful, may have been cancelled, please try again");
                    }
                case StorePurchaseStatus.NetworkError:
                    {
                        throw new Exception("Purchase unsucessful, network error, please try again");
                    }
                case StorePurchaseStatus.ServerError:
                    {
                        throw new Exception("Purchase unsucessful, server error, please try again");

                    }
                default:
                    {
                        return false;
                    }
            }
        }
        catch (Exception exception)
        {
            throw;
        }
    }
}

Code in MauiProgram.cs to create handle;

in class:
public static nint WindowHandle = 0;

before builder.Build:

`
#if WINDOWS

    builder.ConfigureLifecycleEvents(events =>
    {
        // Make sure to add "using Microsoft.Maui.LifecycleEvents;" in the top of the file
        events.AddWindows(windowsLifecycleBuilder =>
        {
            windowsLifecycleBuilder.OnWindowCreated(window =>
            {
                WindowHandle = WinRT.Interop.WindowNative.GetWindowHandle(window);
            });
        });
    });

#endif
`

@brightertools
Copy link
Author

This is includes the check for purchased add-on.

using Windows.Services.Store;

namespace SocialTextEditor.Platforms.Windows;

public static partial class WindowsInAppBilling
{
    /// <summary>
    /// Check if specified add-on product is purchased
    /// </summary>
    /// <param name="productId"></param>
    /// <param name="supressErrors"></param>
    /// <returns></returns>
    /// <exception cref="Exception"></exception>
    // ref: https://learn.microsoft.com/en-us/windows/uwp/monetize/get-license-info-for-apps-and-add-ons
    public static async Task<bool> AddonIsPurchased(string productId, bool supressErrors = true)
    {
        StoreContext storeContext = StoreContext.GetDefault();

        StoreAppLicense appLicense = await storeContext.GetAppLicenseAsync();

        if (appLicense == null)
        {
            if (supressErrors) { return false; }
            
            throw new Exception("There was an error retrieving licensing info");
        }

        if (appLicense.AddOnLicenses == null || appLicense.AddOnLicenses.Count <= 0)
        {
            if (supressErrors) { return false; }

            throw new Exception("No licensed purchases found");
        }

        var productLicensed = appLicense.AddOnLicenses.Any(x => x.Value.InAppOfferToken == productId && x.Value.IsActive);

        if (!productLicensed)
        {
            if (supressErrors) { return false; }

            throw new Exception("Licensed purchase not found");
        }

        return productLicensed;
    }

    /// <summary>
    /// Initiate purchase for specified add-on product id.
    /// </summary>
    /// <param name="productId"></param>
    /// <returns></returns>
    public static async Task<bool> PurchaseAsync(string productId)
    {
        StoreContext storeContext = StoreContext.GetDefault();

        // This is required to prevent invalid window handle exception
        WinRT.Interop.InitializeWithWindow.Initialize(storeContext, MauiProgram.WindowHandle);

        try
        {
            string[] filterList = new string[] { "Consumable", "Durable", "UnmanagedConsumable" };

            // Get the list of purchase options related to this app
            StoreProductQueryResult productQueryResult = await storeContext.GetAssociatedStoreProductsAsync(filterList);

            if (productQueryResult.ExtendedError != null)
            {
                throw new Exception("There was an error getting product options for this application");
            }

            if (productQueryResult.Products.Count <= 0)
            {
                throw new Exception("No configured purchase options found for this application");
            }

            var product = productQueryResult.Products.Values.FirstOrDefault(x => x.InAppOfferToken == productId);

            if (product == null)
            {
                throw new Exception("Purchase option not found for this application");
            }

            StorePurchaseResult purchaseResult = await storeContext.RequestPurchaseAsync(product.StoreId);

            if (purchaseResult.ExtendedError != null)
            {
                throw new Exception("There was an error purchasing the application");
            }

            switch (purchaseResult.Status)
            {
                case StorePurchaseStatus.AlreadyPurchased:
                    {
                        throw new Exception("Already purchased, if you are not seeing all features please try the restore purchase option");
                    }
                case StorePurchaseStatus.Succeeded:
                    {
                        return true;
                    }
                case StorePurchaseStatus.NotPurchased:
                    {
                        throw new Exception("Purchase unsucessful, may have been cancelled, please try again");
                    }
                case StorePurchaseStatus.NetworkError:
                    {
                        throw new Exception("Purchase unsucessful, network error, please try again");
                    }
                case StorePurchaseStatus.ServerError:
                    {
                        throw new Exception("Purchase unsucessful, server error, please try again");

                    }
                default:
                    {
                        return false;
                    }
            }
        }
        catch (Exception)
        {
            throw;
        }
    }
}

@BurkusCat
Copy link

BurkusCat commented Dec 28, 2023

I've not dug deep into this yet but I'm seeing similar issues to those reported by @brightertools . Restoring purchases gives me a 400 error when debugging and a 404 when running the version downloaded from the store:
image

When using the store version, if I press a button to initiate my purchase command, it seems to hang indefinitely (I've left it for minutes and the button stays disabled). Since @brightertools mentioned admin mode, I decided to give that a try (on Windows 10). When I do this, the button disables for a second or two and then re-enables (so it isn't hanging any more). However, it doesn't appear to succeed in this case and as it is the store version I'm not able to debug it.

On Android, purchasing + restoring is working flawlessly for me. @brightertools seemed to suggest that the InAppBillingPlugin doesn't support paid apps with add-ons in the Microsoft Store. Is this the case @jamesmontemagno ? My use case is to have a paid app and separate in-app purchases. I assumed that the app being paid/free wouldn't change how the plugin worked.

EDIT: I see System.ArgumentException: 'Value does not fall within the expected range.' same as @brightertools when using InTestingMode = true. Does the InAppBillingPlugin support WinUI or is that untested so far @jamesmontemagno ?

@brightertools
Copy link
Author

@BurkusCat

For new MAUI/Windows apps I suspect the newest store code (from Windows.Services.Store) will need to be used and InAppBillingPlugin will need to be updated. The windows code is relatively straight forward.

My app is now available in the store, have tested restoring against promo codes, but will need someone to do a full payment to verify a full payment.

@jamesmontemagno
Copy link
Owner

I will have to spend some time on this, i had started a bit... but really need to give it another go atleast for just normal consumables/non-consumables first. Open to PRs :)

@Rikudouensof
Copy link

Rikudouensof commented Sep 5, 2024

I just came here after trying a lot with the Microsoft store. The null response was something else.

This needs to be implemented in the package when possible.

It is hanging on my end

@Rikudouensof
Copy link

Rikudouensof commented Sep 9, 2024

I have implemented it directly.

There are some things to consider.
Windows.Services.Store is best for people who are using Microsoft AD to manage users who are using the application. If one does not do that, it will cause trouble when it comes to verifying payment.

Windows.ApplicationModel.Store can follow the flow of the inappBillingPlugin system.

I implemented the Windows.Services.Store and cannot verify the payment on the Windows Platform.

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

No branches or pull requests

4 participants