From 979564e4e314e074cb2d2a495861abcda8691d2b Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Thu, 4 May 2023 12:09:05 -0700 Subject: [PATCH 01/10] start implementing v5 --- .../InAppBillingTests.Mac.csproj | 4 - .../InAppBillingTests.iOS.csproj | 4 - src/Plugin.InAppBilling/Converters.android.cs | 40 +++++--- .../InAppBilling.android.cs | 15 +-- .../Plugin.InAppBilling.csproj | 11 ++- .../Shared/InAppBillingProduct.shared.cs | 97 ++++++++++--------- .../InAppBillingProductDiscount.shared.cs | 20 ++++ 7 files changed, 112 insertions(+), 79 deletions(-) diff --git a/src/InAppBillingTests/InAppBillingTests.Mac/InAppBillingTests.Mac.csproj b/src/InAppBillingTests/InAppBillingTests.Mac/InAppBillingTests.Mac.csproj index f4816b3..04d3e4f 100644 --- a/src/InAppBillingTests/InAppBillingTests.Mac/InAppBillingTests.Mac.csproj +++ b/src/InAppBillingTests/InAppBillingTests.Mac/InAppBillingTests.Mac.csproj @@ -95,10 +95,6 @@ {6D4D9135-F225-4626-A9CE-32BDF97AEA89} InAppBillingTests - - {C570E25E-259F-4D4C-88F0-B2982815192D} - Plugin.InAppBilling - \ No newline at end of file diff --git a/src/InAppBillingTests/InAppBillingTests.iOS/InAppBillingTests.iOS.csproj b/src/InAppBillingTests/InAppBillingTests.iOS/InAppBillingTests.iOS.csproj index 24f89de..b6b5bb7 100644 --- a/src/InAppBillingTests/InAppBillingTests.iOS/InAppBillingTests.iOS.csproj +++ b/src/InAppBillingTests/InAppBillingTests.iOS/InAppBillingTests.iOS.csproj @@ -159,9 +159,5 @@ {6D4D9135-F225-4626-A9CE-32BDF97AEA89} InAppBillingTests - - {C570E25E-259F-4D4C-88F0-B2982815192D} - Plugin.InAppBilling - \ No newline at end of file diff --git a/src/Plugin.InAppBilling/Converters.android.cs b/src/Plugin.InAppBilling/Converters.android.cs index 134ec7d..521b501 100644 --- a/src/Plugin.InAppBilling/Converters.android.cs +++ b/src/Plugin.InAppBilling/Converters.android.cs @@ -54,27 +54,39 @@ internal static InAppBillingPurchase ToIABPurchase(this PurchaseHistoryRecord pu }; } - internal static InAppBillingProduct ToIAPProduct(this SkuDetails product) + internal static InAppBillingProduct ToIAPProduct(this ProductDetails product) { + var oneTime = product.GetOneTimePurchaseOfferDetails(); + var subs = product.GetSubscriptionOfferDetails()?.Select(s => new SubscriptionOfferDetail + { + BasePlanId = s.BasePlanId, + OfferId = s.OfferId, + OfferTags = s.OfferTags?.ToList(), + OfferToken = s.OfferToken, + PricingPhases = s?.PricingPhases?.PricingPhaseList?.Select(p => + new PricingPhase + { + BillingCycleCount = p.BillingCycleCount, + BillingPeriod = p.BillingPeriod, + FormattedPrice = p.FormattedPrice, + PriceAmountMicros = p.PriceAmountMicros, + PriceCurrencyCode = p.PriceCurrencyCode, + RecurrenceMode = p.RecurrenceMode + }).ToList() + }); + return new InAppBillingProduct { Name = product.Title, Description = product.Description, - CurrencyCode = product.PriceCurrencyCode, - LocalizedPrice = product.Price, - ProductId = product.Sku, - MicrosPrice = product.PriceAmountMicros, + CurrencyCode = oneTime?.PriceCurrencyCode, + LocalizedPrice = oneTime?.FormattedPrice, + ProductId = product.ProductId, + MicrosPrice = oneTime?.PriceAmountMicros ?? 0, + AndroidExtras = new InAppBillingProductAndroidExtras { - SubscriptionPeriod = product.SubscriptionPeriod, - LocalizedIntroductoryPrice = product.IntroductoryPrice, - MicrosIntroductoryPrice = product.IntroductoryPriceAmountMicros, - FreeTrialPeriod = product.FreeTrialPeriod, - IconUrl = product.IconUrl, - IntroductoryPriceCycles = product.IntroductoryPriceCycles, - IntroductoryPricePeriod = product.IntroductoryPricePeriod, - MicrosOriginalPriceAmount = product.OriginalPriceAmountMicros, - OriginalPrice = product.OriginalPrice + } }; } diff --git a/src/Plugin.InAppBilling/InAppBilling.android.cs b/src/Plugin.InAppBilling/InAppBilling.android.cs index 8efbf84..c697b5c 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -157,16 +157,19 @@ public async override Task> GetProductInfoAsync ParseBillingResult(result); } - var skuDetailsParams = SkuDetailsParams.NewBuilder() - .SetType(skuType) - .SetSkusList(productIds) - .Build(); - var skuDetailsResult = await BillingClient.QuerySkuDetailsAsync(skuDetailsParams); + var productList = productIds.Select(p => QueryProductDetailsParams.Product.NewBuilder() + .SetProductType(skuType) + .SetProductId(p) + .Build()).ToList(); + + var skuDetailsParams = QueryProductDetailsParams.NewBuilder().SetProductList(productList); + + var skuDetailsResult = await BillingClient.QueryProductDetailsAsync(skuDetailsParams.Build()); ParseBillingResult(skuDetailsResult?.Result); - return skuDetailsResult.SkuDetails.Select(product => product.ToIAPProduct()); + return skuDetailsResult.ProductDetails.Select(product => product.ToIAPProduct()); } diff --git a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj index 7750022..d8b0b4b 100644 --- a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj +++ b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj @@ -1,6 +1,7 @@  - netstandard2.0;MonoAndroid10.0;Xamarin.iOS10;Xamarin.TVOS10;Xamarin.Mac20;net6.0-android;net6.0-ios;net6.0-maccatalyst;net6.0-tvos;net6.0-macos + + netstandard2.0;MonoAndroid12.0;Xamarin.iOS10;Xamarin.TVOS10;Xamarin.Mac20;net6.0-android;net6.0-ios;net6.0-maccatalyst;net6.0-tvos;net6.0-macos $(TargetFrameworks);uap10.0.19041;net6.0-windows10.0.19041; 9.0 Plugin.InAppBilling @@ -97,14 +98,14 @@ - - + + - - + + diff --git a/src/Plugin.InAppBilling/Shared/InAppBillingProduct.shared.cs b/src/Plugin.InAppBilling/Shared/InAppBillingProduct.shared.cs index a4f24a8..94eeacd 100644 --- a/src/Plugin.InAppBilling/Shared/InAppBillingProduct.shared.cs +++ b/src/Plugin.InAppBilling/Shared/InAppBillingProduct.shared.cs @@ -26,13 +26,13 @@ public class InAppBillingProductAppleExtras public bool IsFamilyShareable { get; set; } /// - /// iOS 11.2: gets information about product discunt + /// iOS 11.2: gets information about product discount /// public InAppBillingProductDiscount IntroductoryOffer { get; set; } = null; /// - /// iOS 12.2: gets information about product discunt + /// iOS 12.2: gets information about product discount /// public List Discounts { get; set; } = null; } @@ -87,51 +87,56 @@ public class InAppBillingProductWindowsExtras public class InAppBillingProductAndroidExtras { /// - /// Subscription period, specified in ISO 8601 format. - /// - public string SubscriptionPeriod { get; set; } - - /// - /// Trial period, specified in ISO 8601 format. - /// - public string FreeTrialPeriod { get; set; } - - /// - /// Icon of the product if present - /// - public string IconUrl { get; set; } - - /// - /// Gets or sets the localized introductory price. - /// - /// The localized introductory price. - public string LocalizedIntroductoryPrice { get; set; } - - /// - /// Number of subscription billing periods for which the user will be given the introductory price, such as 3 - /// - public int IntroductoryPriceCycles { get; set; } - - /// - /// Billing period of the introductory price, specified in ISO 8601 format - /// - public string IntroductoryPricePeriod { get; set; } - - /// - /// Introductory price of the product in micro-units - /// - /// The introductory price. - public Int64 MicrosIntroductoryPrice { get; set; } - - /// - /// Formatted original price of the item, including its currency sign. - /// - public string OriginalPrice { get; set; } - - /// - /// Original price in micro-units, where 1,000,000, micro-units equal one unit of the currency + /// The period details for products that are subscriptions. /// - public long MicrosOriginalPriceAmount { get; set; } + public List SubscriptionOfferDetails { get; set; } + + ///// + ///// Subscription period, specified in ISO 8601 format. + ///// + //public string SubscriptionPeriod { get; set; } + + ///// + ///// Trial period, specified in ISO 8601 format. + ///// + //public string FreeTrialPeriod { get; set; } + + ///// + ///// Icon of the product if present + ///// + //public string IconUrl { get; set; } + + ///// + ///// Gets or sets the localized introductory price. + ///// + ///// The localized introductory price. + //public string LocalizedIntroductoryPrice { get; set; } + + ///// + ///// Number of subscription billing periods for which the user will be given the introductory price, such as 3 + ///// + //public int IntroductoryPriceCycles { get; set; } + + ///// + ///// Billing period of the introductory price, specified in ISO 8601 format + ///// + //public string IntroductoryPricePeriod { get; set; } + + ///// + ///// Introductory price of the product in micro-units + ///// + ///// The introductory price. + //public Int64 MicrosIntroductoryPrice { get; set; } + + ///// + ///// Formatted original price of the item, including its currency sign. + ///// + //public string OriginalPrice { get; set; } + + ///// + ///// Original price in micro-units, where 1,000,000, micro-units equal one unit of the currency + ///// + //public long MicrosOriginalPriceAmount { get; set; } } diff --git a/src/Plugin.InAppBilling/Shared/InAppBillingProductDiscount.shared.cs b/src/Plugin.InAppBilling/Shared/InAppBillingProductDiscount.shared.cs index 36d43f5..32212d4 100644 --- a/src/Plugin.InAppBilling/Shared/InAppBillingProductDiscount.shared.cs +++ b/src/Plugin.InAppBilling/Shared/InAppBillingProductDiscount.shared.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Plugin.InAppBilling { @@ -128,4 +129,23 @@ public enum ProductDiscountType /// Unknown } + + public class SubscriptionOfferDetail + { + public string BasePlanId { get; set; } + public string OfferId { get; set; } + public List OfferTags { get; set; } + public string OfferToken { get; set; } + public List PricingPhases { get; set; } + } + + public class PricingPhase + { + public int BillingCycleCount { get; set; } + public string BillingPeriod { get; set; } + public string FormattedPrice { get; set; } + public long PriceAmountMicros { get; set; } + public string PriceCurrencyCode { get; set; } + public int RecurrenceMode { get; set; } + } } From 6d9a388b13009f7bb544c7b1648dfd41e5a9e0ba Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Thu, 4 May 2023 15:31:00 -0700 Subject: [PATCH 02/10] More updates --- nuget/readme.txt | 5 ++ .../InAppBilling.android.cs | 84 +++++++++++-------- .../Shared/InAppBillingProduct.shared.cs | 2 +- 3 files changed, 57 insertions(+), 34 deletions(-) diff --git a/nuget/readme.txt b/nuget/readme.txt index e621428..3734876 100644 --- a/nuget/readme.txt +++ b/nuget/readme.txt @@ -1,5 +1,10 @@ In-App Billing Plugin for .NET MAUI, Xamarin, & Windows +Version 7.0+ +1.) Major changes to Android product details. Now using Android Billing v4 + +Please read through: https://developer.android.com/google/play/billing/migrate-gpblv5 + Version 5.0+ has significant updates! 1.) We have removed IInAppBillingVerifyPurchase from all methods. All data required to handle this yourself is returned. 2.) iOS ReceiptURL data is avaialble via ReceiptData diff --git a/src/Plugin.InAppBilling/InAppBilling.android.cs b/src/Plugin.InAppBilling/InAppBilling.android.cs index c697b5c..3247b2d 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -9,6 +9,7 @@ using System.Text; using Android.BillingClient.Api; using Android.Content; +using static Android.BillingClient.Api.BillingClient; #if NET using Microsoft.Maui.ApplicationModel; #else @@ -34,7 +35,7 @@ public class InAppBillingImplementation : BaseInAppBilling /// /// The context. Activity Activity => - Platform.CurrentActivity ?? throw new NullReferenceException("Current Activity is null, ensure that the MainActivity.cs file is configuring Xamarin.Essentials in your source code so the In App Billing can use it."); + Platform.CurrentActivity ?? throw new NullReferenceException("Current Activity is null, ensure that the MainActivity.cs file is configuring Xamarin.Essentials/.NET MAUI in your source code so the In App Billing can use it."); Context Context => Android.App.Application.Context; @@ -146,12 +147,12 @@ public async override Task> GetProductInfoAsync var skuType = itemType switch { - ItemType.InAppPurchase => BillingClient.SkuType.Inapp, - ItemType.InAppPurchaseConsumable => BillingClient.SkuType.Inapp, - _ => BillingClient.SkuType.Subs + ItemType.InAppPurchase => BillingClient.ProductType.Inapp, + ItemType.InAppPurchaseConsumable => BillingClient.ProductType.Inapp, + _ => BillingClient.ProductType.Subs }; - if(skuType == BillingClient.SkuType.Subs) + if(skuType == BillingClient.ProductType.Subs) { var result = BillingClient.IsFeatureSupported(BillingClient.FeatureType.Subscriptions); ParseBillingResult(result); @@ -173,23 +174,23 @@ public async override Task> GetProductInfoAsync } - public override Task> GetPurchasesAsync(ItemType itemType) + public override async Task> GetPurchasesAsync(ItemType itemType) { if (BillingClient == null) throw new InAppBillingPurchaseException(PurchaseError.ServiceUnavailable, "You are not connected to the Google Play App store."); var skuType = itemType switch { - ItemType.InAppPurchase => BillingClient.SkuType.Inapp, - ItemType.InAppPurchaseConsumable => BillingClient.SkuType.Inapp, - _ => BillingClient.SkuType.Subs + ItemType.InAppPurchase => BillingClient.ProductType.Inapp, + ItemType.InAppPurchaseConsumable => BillingClient.ProductType.Inapp, + _ => BillingClient.ProductType.Subs }; - var purchasesResult = BillingClient.QueryPurchases(skuType); + var purchasesResult = await BillingClient.QueryPurchasesAsync(QueryPurchasesParams.NewBuilder().SetProductType(skuType).Build()); - ParseBillingResult(purchasesResult.BillingResult); + ParseBillingResult(purchasesResult.Result); - return Task.FromResult(purchasesResult.PurchasesList.Select(p => p.ToIABPurchase())); + return purchasesResult.Purchases.Select(p => p.ToIABPurchase()); } /// @@ -204,9 +205,9 @@ public override async Task> GetPurchasesHistor var skuType = itemType switch { - ItemType.InAppPurchase => BillingClient.SkuType.Inapp, - ItemType.InAppPurchaseConsumable => BillingClient.SkuType.Inapp, - _ => BillingClient.SkuType.Subs + ItemType.InAppPurchase => BillingClient.ProductType.Inapp, + ItemType.InAppPurchaseConsumable => BillingClient.ProductType.Inapp, + _ => BillingClient.ProductType.Subs }; var purchasesResult = await BillingClient.QueryPurchaseHistoryAsync(skuType); @@ -243,7 +244,7 @@ public override async Task UpgradePurchasedSubscriptionAsy async Task UpgradePurchasedSubscriptionInternalAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode) { - var itemType = BillingClient.SkuType.Subs; + var itemType = BillingClient.ProductType.Subs; if (tcsPurchase?.Task != null && !tcsPurchase.Task.IsCompleted) { @@ -293,7 +294,7 @@ async Task UpgradePurchasedSubscriptionInternalAsync(strin //for some reason the data didn't come back if (androidPurchase == null) { - var purchases = await GetPurchasesAsync(itemType == BillingClient.SkuType.Inapp ? ItemType.InAppPurchase : ItemType.Subscription); + var purchases = await GetPurchasesAsync(itemType == BillingClient.ProductType.Inapp ? ItemType.InAppPurchase : ItemType.Subscription); return purchases.FirstOrDefault(p => p.ProductId == newProductId); } @@ -326,58 +327,75 @@ public async override Task PurchaseAsync(string productId, { case ItemType.InAppPurchase: case ItemType.InAppPurchaseConsumable: - return await PurchaseAsync(productId, BillingClient.SkuType.Inapp, obfuscatedAccountId, obfuscatedProfileId); + return await PurchaseAsync(productId, BillingClient.ProductType.Inapp, obfuscatedAccountId, obfuscatedProfileId); case ItemType.Subscription: var result = BillingClient.IsFeatureSupported(BillingClient.FeatureType.Subscriptions); ParseBillingResult(result); - return await PurchaseAsync(productId, BillingClient.SkuType.Subs, obfuscatedAccountId, obfuscatedProfileId); + return await PurchaseAsync(productId, BillingClient.ProductType.Subs, obfuscatedAccountId, obfuscatedProfileId); } return null; } async Task PurchaseAsync(string productSku, string itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null) - { + { - var skuDetailsParams = SkuDetailsParams.NewBuilder() - .SetType(itemType) - .SetSkusList(new List { productSku }) + var productList = QueryProductDetailsParams.Product.NewBuilder() + .SetProductType(itemType) + .SetProductId(productSku) .Build(); - var skuDetailsResult = await BillingClient.QuerySkuDetailsAsync(skuDetailsParams); - ParseBillingResult(skuDetailsResult?.Result); + var skuDetailsParams = QueryProductDetailsParams.NewBuilder().SetProductList(new[] {productList}); + + var skuDetailsResult = await BillingClient.QueryProductDetailsAsync(skuDetailsParams.Build()); + + ParseBillingResult(skuDetailsResult.Result); + + var skuDetails = skuDetailsResult.ProductDetails.FirstOrDefault(); - var skuDetails = skuDetailsResult.SkuDetails.FirstOrDefault(); if (skuDetails == null) throw new ArgumentException($"{productSku} does not exist"); - var flowParamsBuilder = BillingFlowParams.NewBuilder() - .SetSkuDetails(skuDetails); + + var productDetailsParamsList = BillingFlowParams.ProductDetailsParams.NewBuilder() + .SetProductDetails(skuDetails) + //OFFER TOKEN NEEDED? + .Build(); + + var billingFlowParams = BillingFlowParams.NewBuilder() + .SetProductDetailsParamsList(new[] { productDetailsParamsList }); + + if (!string.IsNullOrWhiteSpace(obfuscatedAccountId)) - flowParamsBuilder.SetObfuscatedAccountId(obfuscatedAccountId); + billingFlowParams.SetObfuscatedAccountId(obfuscatedAccountId); if (!string.IsNullOrWhiteSpace(obfuscatedProfileId)) - flowParamsBuilder.SetObfuscatedProfileId(obfuscatedProfileId); + billingFlowParams.SetObfuscatedProfileId(obfuscatedProfileId); + + var flowParams = billingFlowParams.Build(); + - var flowParams = flowParamsBuilder.Build(); tcsPurchase = new TaskCompletionSource<(BillingResult billingResult, IList purchases)>(); + + var responseCode = BillingClient.LaunchBillingFlow(Activity, flowParams); + ParseBillingResult(responseCode); var result = await tcsPurchase.Task; ParseBillingResult(result.billingResult); //we are only buying 1 thing. - var androidPurchase = result.purchases?.FirstOrDefault(p => p.Skus.Contains(productSku)); + var androidPurchase = result.purchases?.FirstOrDefault(p => p.Products.Contains(productSku)); //for some reason the data didn't come back if (androidPurchase == null) { - var purchases = await GetPurchasesAsync(itemType == BillingClient.SkuType.Inapp ? ItemType.InAppPurchase : ItemType.Subscription); + var purchases = await GetPurchasesAsync(itemType == BillingClient.ProductType.Inapp ? ItemType.InAppPurchase : ItemType.Subscription); return purchases.FirstOrDefault(p => p.ProductId == productSku); } diff --git a/src/Plugin.InAppBilling/Shared/InAppBillingProduct.shared.cs b/src/Plugin.InAppBilling/Shared/InAppBillingProduct.shared.cs index 94eeacd..a8f8b59 100644 --- a/src/Plugin.InAppBilling/Shared/InAppBillingProduct.shared.cs +++ b/src/Plugin.InAppBilling/Shared/InAppBillingProduct.shared.cs @@ -89,7 +89,7 @@ public class InAppBillingProductAndroidExtras /// /// The period details for products that are subscriptions. /// - public List SubscriptionOfferDetails { get; set; } + public List SubscriptionOfferDetails { get; set; } ///// ///// Subscription period, specified in ISO 8601 format. From e50687cabc427f623a7c3a583fb71843e64458ab Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Thu, 4 May 2023 15:50:14 -0700 Subject: [PATCH 03/10] Almost finish migration --- .../InAppBilling.android.cs | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/src/Plugin.InAppBilling/InAppBilling.android.cs b/src/Plugin.InAppBilling/InAppBilling.android.cs index 3247b2d..b78a99b 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -37,7 +37,7 @@ public class InAppBillingImplementation : BaseInAppBilling Activity Activity => Platform.CurrentActivity ?? throw new NullReferenceException("Current Activity is null, ensure that the MainActivity.cs file is configuring Xamarin.Essentials/.NET MAUI in your source code so the In App Billing can use it."); - Context Context => Android.App.Application.Context; + Context Context => Application.Context; /// /// Default Constructor for In App Billing Implementation on Android @@ -68,7 +68,7 @@ public override Task ConnectAsync(bool enablePendingPurchases = true) tcsConnect?.TrySetCanceled(); tcsConnect = new TaskCompletionSource(); - BillingClientBuilder = BillingClient.NewBuilder(Context); + BillingClientBuilder = NewBuilder(Context); BillingClientBuilder.SetListener(OnPurchasesUpdated); if (enablePendingPurchases) BillingClient = BillingClientBuilder.EnablePendingPurchases().Build(); @@ -147,14 +147,14 @@ public async override Task> GetProductInfoAsync var skuType = itemType switch { - ItemType.InAppPurchase => BillingClient.ProductType.Inapp, - ItemType.InAppPurchaseConsumable => BillingClient.ProductType.Inapp, - _ => BillingClient.ProductType.Subs + ItemType.InAppPurchase => ProductType.Inapp, + ItemType.InAppPurchaseConsumable => ProductType.Inapp, + _ => ProductType.Subs }; - if(skuType == BillingClient.ProductType.Subs) + if(skuType == ProductType.Subs) { - var result = BillingClient.IsFeatureSupported(BillingClient.FeatureType.Subscriptions); + var result = BillingClient.IsFeatureSupported(FeatureType.Subscriptions); ParseBillingResult(result); } @@ -181,9 +181,9 @@ public override async Task> GetPurchasesAsync( var skuType = itemType switch { - ItemType.InAppPurchase => BillingClient.ProductType.Inapp, - ItemType.InAppPurchaseConsumable => BillingClient.ProductType.Inapp, - _ => BillingClient.ProductType.Subs + ItemType.InAppPurchase => ProductType.Inapp, + ItemType.InAppPurchaseConsumable => ProductType.Inapp, + _ => ProductType.Subs }; var purchasesResult = await BillingClient.QueryPurchasesAsync(QueryPurchasesParams.NewBuilder().SetProductType(skuType).Build()); @@ -205,11 +205,12 @@ public override async Task> GetPurchasesHistor var skuType = itemType switch { - ItemType.InAppPurchase => BillingClient.ProductType.Inapp, - ItemType.InAppPurchaseConsumable => BillingClient.ProductType.Inapp, - _ => BillingClient.ProductType.Subs + ItemType.InAppPurchase => ProductType.Inapp, + ItemType.InAppPurchaseConsumable => ProductType.Inapp, + _ => ProductType.Subs }; + //TODO: Binding needs updated var purchasesResult = await BillingClient.QueryPurchaseHistoryAsync(skuType); @@ -244,22 +245,25 @@ public override async Task UpgradePurchasedSubscriptionAsy async Task UpgradePurchasedSubscriptionInternalAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode) { - var itemType = BillingClient.ProductType.Subs; + var itemType = ProductType.Subs; if (tcsPurchase?.Task != null && !tcsPurchase.Task.IsCompleted) { return null; } - var skuDetailsParams = SkuDetailsParams.NewBuilder() - .SetType(itemType) - .SetSkusList(new List { newProductId }) - .Build(); + var productList = QueryProductDetailsParams.Product.NewBuilder() + .SetProductType(itemType) + .SetProductId(newProductId) + .Build(); - var skuDetailsResult = await BillingClient.QuerySkuDetailsAsync(skuDetailsParams); - ParseBillingResult(skuDetailsResult?.Result); + var skuDetailsParams = QueryProductDetailsParams.NewBuilder().SetProductList(new[] { productList }).Build(); - var skuDetails = skuDetailsResult?.SkuDetails.FirstOrDefault(); + var skuDetailsResult = await BillingClient.QueryProductDetailsAsync(skuDetailsParams); + + ParseBillingResult(skuDetailsResult.Result); + + var skuDetails = skuDetailsResult.ProductDetails.FirstOrDefault(); if (skuDetails == null) throw new ArgumentException($"{newProductId} does not exist"); @@ -271,12 +275,16 @@ async Task UpgradePurchasedSubscriptionInternalAsync(strin //5 - BillingFlowParams.ProrationMode.ImmediateAndChargeFullPrice var updateParams = BillingFlowParams.SubscriptionUpdateParams.NewBuilder() - .SetOldSkuPurchaseToken(purchaseTokenOfOriginalSubscription) - .SetReplaceSkusProrationMode((int)prorationMode) + .SetOldPurchaseToken(purchaseTokenOfOriginalSubscription) + .SetReplaceProrationMode((int)prorationMode) + .Build(); + + var prodDetailsParams = BillingFlowParams.ProductDetailsParams.NewBuilder() + .SetProductDetails(skuDetails) .Build(); var flowParams = BillingFlowParams.NewBuilder() - .SetSkuDetails(skuDetails) + .SetProductDetailsParamsList(new[] { prodDetailsParams }) .SetSubscriptionUpdateParams(updateParams) .Build(); @@ -289,12 +297,12 @@ async Task UpgradePurchasedSubscriptionInternalAsync(strin ParseBillingResult(result.billingResult); //we are only buying 1 thing. - var androidPurchase = result.purchases?.FirstOrDefault(p => p.Skus.Contains(newProductId)); + var androidPurchase = result.purchases?.FirstOrDefault(p => p.Products.Contains(newProductId)); //for some reason the data didn't come back if (androidPurchase == null) { - var purchases = await GetPurchasesAsync(itemType == BillingClient.ProductType.Inapp ? ItemType.InAppPurchase : ItemType.Subscription); + var purchases = await GetPurchasesAsync(itemType == ProductType.Inapp ? ItemType.InAppPurchase : ItemType.Subscription); return purchases.FirstOrDefault(p => p.ProductId == newProductId); } @@ -327,12 +335,12 @@ public async override Task PurchaseAsync(string productId, { case ItemType.InAppPurchase: case ItemType.InAppPurchaseConsumable: - return await PurchaseAsync(productId, BillingClient.ProductType.Inapp, obfuscatedAccountId, obfuscatedProfileId); + return await PurchaseAsync(productId, ProductType.Inapp, obfuscatedAccountId, obfuscatedProfileId); case ItemType.Subscription: - var result = BillingClient.IsFeatureSupported(BillingClient.FeatureType.Subscriptions); + var result = BillingClient.IsFeatureSupported(FeatureType.Subscriptions); ParseBillingResult(result); - return await PurchaseAsync(productId, BillingClient.ProductType.Subs, obfuscatedAccountId, obfuscatedProfileId); + return await PurchaseAsync(productId, ProductType.Subs, obfuscatedAccountId, obfuscatedProfileId); } return null; @@ -395,7 +403,7 @@ async Task PurchaseAsync(string productSku, string itemTyp //for some reason the data didn't come back if (androidPurchase == null) { - var purchases = await GetPurchasesAsync(itemType == BillingClient.ProductType.Inapp ? ItemType.InAppPurchase : ItemType.Subscription); + var purchases = await GetPurchasesAsync(itemType == ProductType.Inapp ? ItemType.InAppPurchase : ItemType.Subscription); return purchases.FirstOrDefault(p => p.ProductId == productSku); } @@ -424,10 +432,11 @@ async Task PurchaseAsync(string productSku, string itemTyp return items; } - //inapp:{Context.PackageName}:{productSku} + /// /// Consume a purchase with a purchase token. + /// in app:{Context.PackageName}:{productSku} /// /// Id or Sku of product /// Original Purchase Token From d3eda74d695e174c2bfed22b594d9f6db6c81ba7 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Wed, 31 May 2023 09:30:41 -0700 Subject: [PATCH 04/10] udpate readme --- nuget/readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nuget/readme.txt b/nuget/readme.txt index 3734876..ac985d9 100644 --- a/nuget/readme.txt +++ b/nuget/readme.txt @@ -1,7 +1,7 @@ In-App Billing Plugin for .NET MAUI, Xamarin, & Windows Version 7.0+ -1.) Major changes to Android product details. Now using Android Billing v4 +1.) Major changes to Android product details. Now using Android Billing v5 Please read through: https://developer.android.com/google/play/billing/migrate-gpblv5 From 69ee19f9df388490747f98101f748f0a21234929 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Wed, 31 May 2023 10:11:16 -0700 Subject: [PATCH 05/10] more code cleanup --- .../InAppBilling.android.cs | 33 ++++++------------- .../Plugin.InAppBilling.csproj | 6 ++-- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/src/Plugin.InAppBilling/InAppBilling.android.cs b/src/Plugin.InAppBilling/InAppBilling.android.cs index b78a99b..f2ddddf 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -3,10 +3,6 @@ using System.Linq; using System.Threading.Tasks; using Android.App; -using Java.Security; -using Java.Security.Spec; -using Java.Lang; -using System.Text; using Android.BillingClient.Api; using Android.Content; using static Android.BillingClient.Api.BillingClient; @@ -18,10 +14,10 @@ namespace Plugin.InAppBilling { - /// - /// Implementation for Feature - /// - [Preserve(AllMembers = true)] + /// + /// Implementation for Feature + /// + [Preserve(AllMembers = true)] public class InAppBillingImplementation : BaseInAppBilling { /// @@ -34,10 +30,10 @@ public class InAppBillingImplementation : BaseInAppBilling /// This is set from the MainApplication.cs file that was laid down by the plugin /// /// The context. - Activity Activity => + static Activity Activity => Platform.CurrentActivity ?? throw new NullReferenceException("Current Activity is null, ensure that the MainActivity.cs file is configuring Xamarin.Essentials/.NET MAUI in your source code so the In App Billing can use it."); - Context Context => Application.Context; + static Context Context => Application.Context; /// /// Default Constructor for In App Billing Implementation on Android @@ -263,10 +259,7 @@ async Task UpgradePurchasedSubscriptionInternalAsync(strin ParseBillingResult(skuDetailsResult.Result); - var skuDetails = skuDetailsResult.ProductDetails.FirstOrDefault(); - - if (skuDetails == null) - throw new ArgumentException($"{newProductId} does not exist"); + var skuDetails = skuDetailsResult.ProductDetails.FirstOrDefault() ?? throw new ArgumentException($"{newProductId} does not exist"); //1 - BillingFlowParams.ProrationMode.ImmediateWithTimeProration //2 - BillingFlowParams.ProrationMode.ImmediateAndChargeProratedPrice @@ -288,7 +281,7 @@ async Task UpgradePurchasedSubscriptionInternalAsync(strin .SetSubscriptionUpdateParams(updateParams) .Build(); - tcsPurchase = new TaskCompletionSource<(BillingResult billingResult, IList purchases)>(); + tcsPurchase = new TaskCompletionSource<(BillingResult billingResult, IList purchases)>(); var responseCode = BillingClient.LaunchBillingFlow(Activity, flowParams); ParseBillingResult(responseCode); @@ -360,13 +353,7 @@ async Task PurchaseAsync(string productSku, string itemTyp ParseBillingResult(skuDetailsResult.Result); - var skuDetails = skuDetailsResult.ProductDetails.FirstOrDefault(); - - - if (skuDetails == null) - throw new ArgumentException($"{productSku} does not exist"); - - + var skuDetails = skuDetailsResult.ProductDetails.FirstOrDefault() ?? throw new ArgumentException($"{productSku} does not exist"); var productDetailsParamsList = BillingFlowParams.ProductDetailsParams.NewBuilder() .SetProductDetails(skuDetails) //OFFER TOKEN NEEDED? @@ -459,7 +446,7 @@ public override async Task ConsumePurchaseAsync(string productId, string t return ParseBillingResult(result.BillingResult); } - bool ParseBillingResult(BillingResult result) + static bool ParseBillingResult(BillingResult result) { if(result == null) throw new InAppBillingPurchaseException(PurchaseError.GeneralError); diff --git a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj index d8b0b4b..c201ec7 100644 --- a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj +++ b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj @@ -98,14 +98,14 @@ - - + + - + From c13b7c932e71afe18545acbeb9a4bcf25ffa9f2a Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Wed, 31 May 2023 10:12:20 -0700 Subject: [PATCH 06/10] 10 lang --- src/Plugin.InAppBilling/Plugin.InAppBilling.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj index c201ec7..010af4f 100644 --- a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj +++ b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj @@ -3,7 +3,7 @@ netstandard2.0;MonoAndroid12.0;Xamarin.iOS10;Xamarin.TVOS10;Xamarin.Mac20;net6.0-android;net6.0-ios;net6.0-maccatalyst;net6.0-tvos;net6.0-macos $(TargetFrameworks);uap10.0.19041;net6.0-windows10.0.19041; - 9.0 + 10.0 Plugin.InAppBilling Plugin.InAppBilling $(AssemblyName) ($(TargetFramework)) From 4348a8f6ce271ed71928265b9a97923a1aa90a35 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Wed, 31 May 2023 10:31:56 -0700 Subject: [PATCH 07/10] Updates for v6 --- src/Plugin.InAppBilling/InAppBilling.android.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Plugin.InAppBilling/InAppBilling.android.cs b/src/Plugin.InAppBilling/InAppBilling.android.cs index f2ddddf..afbbdce 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -6,6 +6,7 @@ using Android.BillingClient.Api; using Android.Content; using static Android.BillingClient.Api.BillingClient; +using BillingResponseCode = Android.BillingClient.Api.BillingResponseCode; #if NET using Microsoft.Maui.ApplicationModel; #else @@ -182,7 +183,8 @@ public override async Task> GetPurchasesAsync( _ => ProductType.Subs }; - var purchasesResult = await BillingClient.QueryPurchasesAsync(QueryPurchasesParams.NewBuilder().SetProductType(skuType).Build()); + var query = QueryPurchasesParams.NewBuilder().SetProductType(skuType).Build(); + var purchasesResult = await BillingClient.QueryPurchasesAsync(query); ParseBillingResult(purchasesResult.Result); @@ -271,9 +273,10 @@ async Task UpgradePurchasedSubscriptionInternalAsync(strin .SetOldPurchaseToken(purchaseTokenOfOriginalSubscription) .SetReplaceProrationMode((int)prorationMode) .Build(); - + var prodDetailsParams = BillingFlowParams.ProductDetailsParams.NewBuilder() .SetProductDetails(skuDetails) + .SetOfferToken(skuDetails.GetSubscriptionOfferDetails()?.FirstOrDefault()?.OfferToken) .Build(); var flowParams = BillingFlowParams.NewBuilder() From 4a092d541360ba2d2e949dd6c7e535347dea6eaf Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Wed, 31 May 2023 10:33:15 -0700 Subject: [PATCH 08/10] update readme --- nuget/readme.txt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/nuget/readme.txt b/nuget/readme.txt index ac985d9..5ceb5f5 100644 --- a/nuget/readme.txt +++ b/nuget/readme.txt @@ -1,9 +1,12 @@ In-App Billing Plugin for .NET MAUI, Xamarin, & Windows -Version 7.0+ -1.) Major changes to Android product details. Now using Android Billing v5 +Version 7.0+ - Major Android updates +1.) You must compile and target against Android 12 or higher +2.) Android: Now using Android Billing v6 +3.) Android: Major changes to Android product details, subscriptions, and more + +Please read through: https://developer.android.com/google/play/billing/migrate-gpblv6 -Please read through: https://developer.android.com/google/play/billing/migrate-gpblv5 Version 5.0+ has significant updates! 1.) We have removed IInAppBillingVerifyPurchase from all methods. All data required to handle this yourself is returned. From e8c4e4adb5bae1bc16d33bf4687cebb29a5b8c3e Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Wed, 31 May 2023 10:36:07 -0700 Subject: [PATCH 09/10] tweak resonse code --- src/Plugin.InAppBilling/InAppBilling.android.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Plugin.InAppBilling/InAppBilling.android.cs b/src/Plugin.InAppBilling/InAppBilling.android.cs index afbbdce..f840e33 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -454,6 +454,9 @@ static bool ParseBillingResult(BillingResult result) if(result == null) throw new InAppBillingPurchaseException(PurchaseError.GeneralError); + if ((int)result.ResponseCode == Android.BillingClient.Api.BillingClient.BillingResponseCode.NetworkError) + throw new InAppBillingPurchaseException(PurchaseError.ServiceTimeout);//Network connection is down + return result.ResponseCode switch { BillingResponseCode.Ok => true, From 3e9ab6b9e87d7b8038736d2c7bdbe308a5b2041b Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Wed, 31 May 2023 23:10:05 -0700 Subject: [PATCH 10/10] Set offer token --- .../InAppBilling.android.cs | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/Plugin.InAppBilling/InAppBilling.android.cs b/src/Plugin.InAppBilling/InAppBilling.android.cs index f840e33..b80ed71 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -149,7 +149,7 @@ public async override Task> GetProductInfoAsync _ => ProductType.Subs }; - if(skuType == ProductType.Subs) + if (skuType == ProductType.Subs) { var result = BillingClient.IsFeatureSupported(FeatureType.Subscriptions); ParseBillingResult(result); @@ -170,8 +170,8 @@ public async override Task> GetProductInfoAsync return skuDetailsResult.ProductDetails.Select(product => product.ToIAPProduct()); } - - public override async Task> GetPurchasesAsync(ItemType itemType) + + public override async Task> GetPurchasesAsync(ItemType itemType) { if (BillingClient == null) throw new InAppBillingPurchaseException(PurchaseError.ServiceUnavailable, "You are not connected to the Google Play App store."); @@ -222,7 +222,7 @@ public override async Task> GetPurchasesHistor /// Purchase token of original subscription /// Proration mode (1 - ImmediateWithTimeProration, 2 - ImmediateAndChargeProratedPrice, 3 - ImmediateWithoutProration, 4 - Deferred) /// Purchase details - public override async Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription,SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration) + public override async Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration) { if (BillingClient == null || !IsConnected) { @@ -273,7 +273,7 @@ async Task UpgradePurchasedSubscriptionInternalAsync(strin .SetOldPurchaseToken(purchaseTokenOfOriginalSubscription) .SetReplaceProrationMode((int)prorationMode) .Build(); - + var prodDetailsParams = BillingFlowParams.ProductDetailsParams.NewBuilder() .SetProductDetails(skuDetails) .SetOfferToken(skuDetails.GetSubscriptionOfferDetails()?.FirstOrDefault()?.OfferToken) @@ -350,16 +350,20 @@ async Task PurchaseAsync(string productSku, string itemTyp .SetProductId(productSku) .Build(); - var skuDetailsParams = QueryProductDetailsParams.NewBuilder().SetProductList(new[] {productList}); + var skuDetailsParams = QueryProductDetailsParams.NewBuilder().SetProductList(new[] { productList }); var skuDetailsResult = await BillingClient.QueryProductDetailsAsync(skuDetailsParams.Build()); ParseBillingResult(skuDetailsResult.Result); var skuDetails = skuDetailsResult.ProductDetails.FirstOrDefault() ?? throw new ArgumentException($"{productSku} does not exist"); - var productDetailsParamsList = BillingFlowParams.ProductDetailsParams.NewBuilder() + var productDetailsParamsList = itemType == ProductType.Subs ? + BillingFlowParams.ProductDetailsParams.NewBuilder() + .SetProductDetails(skuDetails) + .SetOfferToken(skuDetails.GetSubscriptionOfferDetails()?.FirstOrDefault()?.OfferToken) + .Build() + : BillingFlowParams.ProductDetailsParams.NewBuilder() .SetProductDetails(skuDetails) - //OFFER TOKEN NEEDED? .Build(); var billingFlowParams = BillingFlowParams.NewBuilder() @@ -382,7 +386,7 @@ async Task PurchaseAsync(string productSku, string itemTyp var responseCode = BillingClient.LaunchBillingFlow(Activity, flowParams); - ParseBillingResult(responseCode); + ParseBillingResult(responseCode); var result = await tcsPurchase.Task; ParseBillingResult(result.billingResult); @@ -438,7 +442,7 @@ public override async Task ConsumePurchaseAsync(string productId, string t throw new InAppBillingPurchaseException(PurchaseError.ServiceUnavailable, "You are not connected to the Google Play App store."); } - + var consumeParams = ConsumeParams.NewBuilder() .SetPurchaseToken(transactionIdentifier) .Build(); @@ -446,12 +450,12 @@ public override async Task ConsumePurchaseAsync(string productId, string t var result = await BillingClient.ConsumeAsync(consumeParams); - return ParseBillingResult(result.BillingResult); + return ParseBillingResult(result.BillingResult); } static bool ParseBillingResult(BillingResult result) { - if(result == null) + if (result == null) throw new InAppBillingPurchaseException(PurchaseError.GeneralError); if ((int)result.ResponseCode == Android.BillingClient.Api.BillingClient.BillingResponseCode.NetworkError) @@ -473,7 +477,7 @@ static bool ParseBillingResult(BillingResult result) BillingResponseCode.ItemUnavailable => throw new InAppBillingPurchaseException(PurchaseError.ItemUnavailable), _ => false, }; - } + } } } - +