2

I'm looking for documentation on this and I'm not finding it.

The main things I need to accomplish are:

  • Allow the user to purchase an add-on through the Microsoft Store.
  • Determine if they've already purchased an add-on so the appropriate application features can be enabled.

These two things can be accomplished easily for a UWP app as described here: https://learn.microsoft.com/en-us/windows/uwp/monetize/enable-subscription-add-ons-for-your-app#code-examples

But I can't find an equivalent for .NET MAUI.

Steve Wortham
  • 21,740
  • 5
  • 68
  • 90
  • 1
    MAUI is cross platform, so you would need to implement it per platform for each respective store – Jason Jan 23 '23 at 17:25
  • For example, in your project's `Windows` folder, write code you can find by google `windows app sdk microsoft store in app purchase`. To access this from your main Maui code, google `maui platform specific code`. – ToolmakerSteve Jan 23 '23 at 20:07
  • Do you only need to integrate billing for the Microsoft Store? For Android and iOS you could just use James Montemagno's plugin: https://github.com/jamesmontemagno/InAppBillingPlugin – Julian Jan 23 '23 at 21:29
  • Steve, i need the same. Post the solution when found... brgrds – Isidoros Moulas Jan 24 '23 at 10:37
  • For now I only need to figure this out for the Microsoft Store. In the future I'll add this to the Apple Store for MacOS but I'm not there yet. – Steve Wortham Jan 24 '23 at 16:14

1 Answers1

1

So in fact you can use the UWP code examples from Microsoft. The trick is to wrap everything that's Windows specific in a statement like this:

#if WINDOWS
    ...
#endif

So my code looks like this:

#if WINDOWS
    using Windows.Services.Store;
#endif

namespace MyProject.Data
{
    public class AddOnService
    {
#if WINDOWS
            private StoreContext context = null;
            StoreProduct subscriptionStoreProduct;
    
            // Assign this variable to the Store ID of your subscription add-on.
            private string subscriptionStoreId = "INSERT-STORE-ID";  
    
            public async Task<bool> SetupSubscriptionInfoAsync()
            {
                if (context == null)
                {
                    context = StoreContext.GetDefault();
                    // If your app is a desktop app that uses the Desktop Bridge, you
                    // may need additional code to configure the StoreContext object.
                    // For more info, see https://aka.ms/storecontext-for-desktop.
                }
    
                bool userOwnsSubscription = await CheckIfUserHasSubscriptionAsync();
                if (userOwnsSubscription)
                {
                    // Unlock all the subscription add-on features here.
                    return true;
                }
    
                // Get the StoreProduct that represents the subscription add-on.
                subscriptionStoreProduct = await GetSubscriptionProductAsync();
                if (subscriptionStoreProduct == null)
                {
                    return true;
                }
    
                // Check if the first SKU is a trial and notify the customer that a trial is available.
                // If a trial is available, the Skus array will always have 2 purchasable SKUs and the
                // first one is the trial. Otherwise, this array will only have one SKU.
                StoreSku sku = subscriptionStoreProduct.Skus[0];
                if (sku.SubscriptionInfo.HasTrialPeriod)
                {
                    // You can display the subscription trial info to the customer here. You can use 
                    // sku.SubscriptionInfo.TrialPeriod and sku.SubscriptionInfo.TrialPeriodUnit 
                    // to get the trial details.
                }
                else
                {
                    // You can display the subscription purchase info to the customer here. You can use 
                    // sku.SubscriptionInfo.BillingPeriod and sku.SubscriptionInfo.BillingPeriodUnit
                    // to provide the renewal details.
                }
    
                // Prompt the customer to purchase the subscription.
                await PromptUserToPurchaseAsync();
    
                return false;
            }
    
            private async Task<StoreProduct> GetSubscriptionProductAsync()
            {
                // Load the sellable add-ons for this app and check if the trial is still 
                // available for this customer. If they previously acquired a trial they won't 
                // be able to get a trial again, and the StoreProduct.Skus property will 
                // only contain one SKU.
                StoreProductQueryResult result =
                    await context.GetAssociatedStoreProductsAsync(new string[] { "Durable" });
    
                if (result.ExtendedError != null)
                {
                    System.Diagnostics.Debug.WriteLine("Something went wrong while getting the add-ons. " +
                        "ExtendedError:" + result.ExtendedError);
                    return null;
                }
    
                // Look for the product that represents the subscription.
                foreach (var item in result.Products)
                {
                    StoreProduct product = item.Value;
                    if (product.StoreId == subscriptionStoreId)
                    {
                        return product;
                    }
                }
    
                System.Diagnostics.Debug.WriteLine("The subscription was not found.");
                return null;
            }
    
            private async Task<bool> CheckIfUserHasSubscriptionAsync()
            {
                StoreAppLicense appLicense = await context.GetAppLicenseAsync();
    
                // Check if the customer has the rights to the subscription.
                foreach (var addOnLicense in appLicense.AddOnLicenses)
                {
                    StoreLicense license = addOnLicense.Value;
                    if (license.SkuStoreId.StartsWith(subscriptionStoreId))
                    {
                        if (license.IsActive)
                        {
                            // The expiration date is available in the license.ExpirationDate property.
                            return true;
                        }
                    }
                }
    
                // The customer does not have a license to the subscription.
                return false;
            }
    
            private async Task PromptUserToPurchaseAsync()
            {
                // Request a purchase of the subscription product. If a trial is available it will be offered 
                // to the customer. Otherwise, the non-trial SKU will be offered.
                StorePurchaseResult result = await subscriptionStoreProduct.RequestPurchaseAsync();
    
                // Capture the error message for the operation, if any.
                string extendedError = string.Empty;
                if (result.ExtendedError != null)
                {
                    extendedError = result.ExtendedError.Message;
                }
    
                switch (result.Status)
                {
                    case StorePurchaseStatus.Succeeded:
                        // Show a UI to acknowledge that the customer has purchased your subscription 
                        // and unlock the features of the subscription. 
                        break;
    
                    case StorePurchaseStatus.NotPurchased:
                        System.Diagnostics.Debug.WriteLine("The purchase did not complete. " +
                            "The customer may have cancelled the purchase. ExtendedError: " + extendedError);
                        break;
    
                    case StorePurchaseStatus.ServerError:
                    case StorePurchaseStatus.NetworkError:
                        System.Diagnostics.Debug.WriteLine("The purchase was unsuccessful due to a server or network error. " +
                            "ExtendedError: " + extendedError);
                        break;
    
                    case StorePurchaseStatus.AlreadyPurchased:
                        System.Diagnostics.Debug.WriteLine("The customer already owns this subscription." +
                                "ExtendedError: " + extendedError);
                        break;
                }
            }
#else
        public async Task<bool> SetupSubscriptionInfoAsync()
        {
            return false;
        }
#endif
    }
}

I'll be modifying this some to suit my needs but I was able to debug it and step through the code to verify that the code is indeed executing as expected on Windows.

Julian
  • 5,290
  • 1
  • 17
  • 40
Steve Wortham
  • 21,740
  • 5
  • 68
  • 90
  • 1
    You could also use the built-in multi-targeting and add the code only into the Windows-specific directory of your MAUI app. That way, you won't need the preprocessor directives. For that, you could create an interface and stub implementations for the other stores and later fill them with the required implementation for each platform: https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/configure-multi-targeting – Julian Jan 24 '23 at 16:42