From 979564e4e314e074cb2d2a495861abcda8691d2b Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Thu, 4 May 2023 12:09:05 -0700 Subject: [PATCH 01/16] 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/16] 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/16] 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 @@ public async override Task> FinalizePurch 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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, }; - } + } } } - + From 1c6009cfd361675192bbd20ed603556fecb3fab3 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Thu, 1 Jun 2023 09:43:40 -0700 Subject: [PATCH 11/16] add subs --- src/Plugin.InAppBilling/Converters.android.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Plugin.InAppBilling/Converters.android.cs b/src/Plugin.InAppBilling/Converters.android.cs index 521b501..889fc54 100644 --- a/src/Plugin.InAppBilling/Converters.android.cs +++ b/src/Plugin.InAppBilling/Converters.android.cs @@ -73,7 +73,7 @@ internal static InAppBillingProduct ToIAPProduct(this ProductDetails product) PriceCurrencyCode = p.PriceCurrencyCode, RecurrenceMode = p.RecurrenceMode }).ToList() - }); + }).ToList(); return new InAppBillingProduct { @@ -83,10 +83,9 @@ internal static InAppBillingProduct ToIAPProduct(this ProductDetails product) LocalizedPrice = oneTime?.FormattedPrice, ProductId = product.ProductId, MicrosPrice = oneTime?.PriceAmountMicros ?? 0, - AndroidExtras = new InAppBillingProductAndroidExtras { - + SubscriptionOfferDetails = subs } }; } From 252979f064c134cbd68fe471a66696b375de7c3f Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Fri, 2 Jun 2023 20:33:24 -0700 Subject: [PATCH 12/16] Return info of first sub into default info for simplicity --- src/Plugin.InAppBilling/Converters.android.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Plugin.InAppBilling/Converters.android.cs b/src/Plugin.InAppBilling/Converters.android.cs index 889fc54..adc2bc9 100644 --- a/src/Plugin.InAppBilling/Converters.android.cs +++ b/src/Plugin.InAppBilling/Converters.android.cs @@ -17,9 +17,9 @@ internal static InAppBillingPurchase ToIABPurchase(this Purchase purchase) Signature = purchase.Signature, IsAcknowledged = purchase.IsAcknowledged, Payload = purchase.DeveloperPayload, - ProductId = purchase.Skus.FirstOrDefault(), + ProductId = purchase.Products?.FirstOrDefault(), Quantity = purchase.Quantity, - ProductIds = purchase.Skus, + ProductIds = purchase.Products, PurchaseToken = purchase.PurchaseToken, TransactionDateUtc = DateTimeOffset.FromUnixTimeMilliseconds(purchase.PurchaseTime).DateTime, ObfuscatedAccountId = purchase.AccountIdentifiers?.ObfuscatedAccountId, @@ -44,9 +44,9 @@ internal static InAppBillingPurchase ToIABPurchase(this PurchaseHistoryRecord pu OriginalJson = purchase.OriginalJson, Signature = purchase.Signature, Payload = purchase.DeveloperPayload, - ProductId = purchase.Skus.FirstOrDefault(), + ProductId = purchase.Products?.FirstOrDefault(), Quantity = purchase.Quantity, - ProductIds = purchase.Skus, + ProductIds = purchase.Products, PurchaseToken = purchase.PurchaseToken, TransactionDateUtc = DateTimeOffset.FromUnixTimeMilliseconds(purchase.PurchaseTime).DateTime, State = PurchaseState.Unknown, @@ -74,15 +74,17 @@ internal static InAppBillingProduct ToIAPProduct(this ProductDetails product) RecurrenceMode = p.RecurrenceMode }).ToList() }).ToList(); + + var firstSub = subs?.FirstOrDefault()?.PricingPhases?.FirstOrDefault(); return new InAppBillingProduct { Name = product.Title, Description = product.Description, - CurrencyCode = oneTime?.PriceCurrencyCode, - LocalizedPrice = oneTime?.FormattedPrice, + CurrencyCode = oneTime?.PriceCurrencyCode ?? firstSub?.PriceCurrencyCode, + LocalizedPrice = oneTime?.FormattedPrice ?? firstSub?.FormattedPrice, ProductId = product.ProductId, - MicrosPrice = oneTime?.PriceAmountMicros ?? 0, + MicrosPrice = oneTime?.PriceAmountMicros ?? firstSub?.PriceAmountMicros ?? 0, AndroidExtras = new InAppBillingProductAndroidExtras { SubscriptionOfferDetails = subs From c739fe4fd8b0549ab8f10f48389f1efbba9e718d Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Fri, 2 Jun 2023 23:05:00 -0700 Subject: [PATCH 13/16] Fixes #533 --- src/Plugin.InAppBilling/InAppBilling.apple.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugin.InAppBilling/InAppBilling.apple.cs b/src/Plugin.InAppBilling/InAppBilling.apple.cs index c50c773..2e87df7 100644 --- a/src/Plugin.InAppBilling/InAppBilling.apple.cs +++ b/src/Plugin.InAppBilling/InAppBilling.apple.cs @@ -325,7 +325,7 @@ public async override Task PurchaseAsync(string productId, var purchase = new InAppBillingPurchase { - TransactionDateUtc = reference.AddSeconds(p.TransactionDate.SecondsSinceReferenceDate), + TransactionDateUtc = reference.AddSeconds(p.TransactionDate?.SecondsSinceReferenceDate ?? 0), Id = p.TransactionIdentifier, TransactionIdentifier = p.TransactionIdentifier, ProductId = p.Payment?.ProductIdentifier ?? string.Empty, From 015de2701812b85710b935e666bb8eb66cdc14f2 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Fri, 2 Jun 2023 23:10:33 -0700 Subject: [PATCH 14/16] Add in offer token for purchasnig --- src/Plugin.InAppBilling/InAppBilling.android.cs | 8 ++++---- src/Plugin.InAppBilling/InAppBilling.apple.cs | 2 +- src/Plugin.InAppBilling/InAppBilling.uwp.cs | 2 +- src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs | 2 +- src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Plugin.InAppBilling/InAppBilling.android.cs b/src/Plugin.InAppBilling/InAppBilling.android.cs index b80ed71..9b096ed 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -313,7 +313,7 @@ async Task UpgradePurchasedSubscriptionInternalAsync(strin /// Specifies an optional obfuscated string that is uniquely associated with the user's account in your app. /// Specifies an optional obfuscated string that is uniquely associated with the user's profile in your app. /// - public async override Task PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null) + public async override Task PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null, string subOfferToken = null) { if (BillingClient == null || !IsConnected) { @@ -336,13 +336,13 @@ public async override Task PurchaseAsync(string productId, var result = BillingClient.IsFeatureSupported(FeatureType.Subscriptions); ParseBillingResult(result); - return await PurchaseAsync(productId, ProductType.Subs, obfuscatedAccountId, obfuscatedProfileId); + return await PurchaseAsync(productId, ProductType.Subs, obfuscatedAccountId, obfuscatedProfileId, subOfferToken); } return null; } - async Task PurchaseAsync(string productSku, string itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null) + async Task PurchaseAsync(string productSku, string itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null, string subOfferToken = null) { var productList = QueryProductDetailsParams.Product.NewBuilder() @@ -360,7 +360,7 @@ async Task PurchaseAsync(string productSku, string itemTyp var productDetailsParamsList = itemType == ProductType.Subs ? BillingFlowParams.ProductDetailsParams.NewBuilder() .SetProductDetails(skuDetails) - .SetOfferToken(skuDetails.GetSubscriptionOfferDetails()?.FirstOrDefault()?.OfferToken) + .SetOfferToken(subOfferToken ?? skuDetails.GetSubscriptionOfferDetails()?.FirstOrDefault()?.OfferToken ?? string.Empty) .Build() : BillingFlowParams.ProductDetailsParams.NewBuilder() .SetProductDetails(skuDetails) diff --git a/src/Plugin.InAppBilling/InAppBilling.apple.cs b/src/Plugin.InAppBilling/InAppBilling.apple.cs index 2e87df7..0b080fc 100644 --- a/src/Plugin.InAppBilling/InAppBilling.apple.cs +++ b/src/Plugin.InAppBilling/InAppBilling.apple.cs @@ -315,7 +315,7 @@ static SKPaymentTransaction FindOriginalTransaction(SKPaymentTransaction transac /// Specifies an optional obfuscated string that is uniquely associated with the user's account in your app. /// Specifies an optional obfuscated string that is uniquely associated with the user's profile in your app. /// - public async override Task PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null) + public async override Task PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null, string subOfferToken = null) { Init(); var p = await PurchaseAsync(productId, itemType, obfuscatedAccountId); diff --git a/src/Plugin.InAppBilling/InAppBilling.uwp.cs b/src/Plugin.InAppBilling/InAppBilling.uwp.cs index d44992f..33d0110 100644 --- a/src/Plugin.InAppBilling/InAppBilling.uwp.cs +++ b/src/Plugin.InAppBilling/InAppBilling.uwp.cs @@ -94,7 +94,7 @@ public async override Task> GetPurchasesAsync( /// Specifies an optional obfuscated string that is uniquely associated with the user's profile in your app. /// /// If an error occurs during processing - public async override Task PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null) + public async override Task PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null, string subOfferToken = null) { // Get purchase result from store or simulator var purchaseResult = await CurrentAppMock.RequestProductPurchaseAsync(InTestingMode, productId); diff --git a/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs b/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs index 929bdc5..b0c5f1e 100644 --- a/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs +++ b/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs @@ -84,7 +84,7 @@ public abstract class BaseInAppBilling : IInAppBilling, IDisposable /// Specifies an optional obfuscated string that is uniquely associated with the user's profile in your app. /// Purchase details /// If an error occurs during processing - public abstract Task PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null); + public abstract Task PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null, string subOfferToken = null); /// /// (Android specific) Upgrade/Downgrade a previously purchased subscription diff --git a/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs b/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs index 8685512..6c1a71a 100644 --- a/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs +++ b/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs @@ -85,7 +85,7 @@ public interface IInAppBilling : IDisposable /// Android: Specifies an optional obfuscated string that is uniquely associated with the user's profile in your app. /// Purchase details /// If an error occurs during processing - Task PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null); + Task PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null, string subOfferToken = null); /// /// (Android specific) Upgrade/Downgrade a previously purchased subscription From cf7cc36a9cd909b4364ac3c4d19995e6183abd29 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Sat, 3 Jun 2023 21:20:21 -0700 Subject: [PATCH 15/16] Fixes #513 --- 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 9b096ed..956c758 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -326,6 +326,9 @@ public async override Task PurchaseAsync(string productId, { return null; } + + if(!string.IsNullOrWhiteSpace(obfuscatedProfileId) && string.IsNullOrWhiteSpace(obfuscatedAccountId)) + throw new ArgumentNullException("You must set an account id if you are setting a profile id"); switch (itemType) { From 95b2d44d1ed14c01b191e0a6361848ab2f9cdb44 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Sun, 4 Jun 2023 09:34:37 -0700 Subject: [PATCH 16/16] Fixes #535 Add more info to exception and add way to filter out invalid products --- src/Plugin.InAppBilling/Converters.android.cs | 2 +- .../InAppBilling.android.cs | 6 ++--- src/Plugin.InAppBilling/InAppBilling.apple.cs | 25 +++++++++++++------ .../Shared/BaseInAppBilling.shared.cs | 6 +++++ .../Shared/IInAppBilling.shared.cs | 6 +++++ .../Shared/InAppBillingExceptions.shared.cs | 14 +++++++++++ 6 files changed, 47 insertions(+), 12 deletions(-) diff --git a/src/Plugin.InAppBilling/Converters.android.cs b/src/Plugin.InAppBilling/Converters.android.cs index adc2bc9..d3c7132 100644 --- a/src/Plugin.InAppBilling/Converters.android.cs +++ b/src/Plugin.InAppBilling/Converters.android.cs @@ -76,7 +76,7 @@ internal static InAppBillingProduct ToIAPProduct(this ProductDetails product) }).ToList(); var firstSub = subs?.FirstOrDefault()?.PricingPhases?.FirstOrDefault(); - + return new InAppBillingProduct { Name = product.Title, diff --git a/src/Plugin.InAppBilling/InAppBilling.android.cs b/src/Plugin.InAppBilling/InAppBilling.android.cs index 9b096ed..173f02e 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -164,7 +164,7 @@ public async override Task> GetProductInfoAsync var skuDetailsParams = QueryProductDetailsParams.NewBuilder().SetProductList(productList); var skuDetailsResult = await BillingClient.QueryProductDetailsAsync(skuDetailsParams.Build()); - ParseBillingResult(skuDetailsResult?.Result); + ParseBillingResult(skuDetailsResult?.Result, IgnoreInvalidProducts); return skuDetailsResult.ProductDetails.Select(product => product.ToIAPProduct()); @@ -453,7 +453,7 @@ public override async Task ConsumePurchaseAsync(string productId, string t return ParseBillingResult(result.BillingResult); } - static bool ParseBillingResult(BillingResult result) + static bool ParseBillingResult(BillingResult result, bool ignoreInvalidProducts = false) { if (result == null) throw new InAppBillingPurchaseException(PurchaseError.GeneralError); @@ -474,7 +474,7 @@ static bool ParseBillingResult(BillingResult result) BillingResponseCode.Error => throw new InAppBillingPurchaseException(PurchaseError.GeneralError),//Generic Error BillingResponseCode.FeatureNotSupported => throw new InAppBillingPurchaseException(PurchaseError.FeatureNotSupported), BillingResponseCode.ItemAlreadyOwned => throw new InAppBillingPurchaseException(PurchaseError.AlreadyOwned), - BillingResponseCode.ItemUnavailable => throw new InAppBillingPurchaseException(PurchaseError.ItemUnavailable), + BillingResponseCode.ItemUnavailable => ignoreInvalidProducts ? false : throw new InAppBillingPurchaseException(PurchaseError.ItemUnavailable), _ => false, }; } diff --git a/src/Plugin.InAppBilling/InAppBilling.apple.cs b/src/Plugin.InAppBilling/InAppBilling.apple.cs index 0b080fc..b6f2767 100644 --- a/src/Plugin.InAppBilling/InAppBilling.apple.cs +++ b/src/Plugin.InAppBilling/InAppBilling.apple.cs @@ -211,7 +211,7 @@ Task> GetProductAsync(string[] productId) { var productIdentifiers = NSSet.MakeNSObjectSet(productId.Select(i => new NSString(i)).ToArray()); - var productRequestDelegate = new ProductRequestDelegate(); + var productRequestDelegate = new ProductRequestDelegate(IgnoreInvalidProducts); //set up product request for in-app purchase var productsRequest = new SKProductsRequest(productIdentifiers) @@ -224,7 +224,7 @@ Task> GetProductAsync(string[] productId) } /// - /// Get app purchaes + /// Get app purchase /// /// /// @@ -625,6 +625,12 @@ public override void Dispose(bool disposing) [Preserve(AllMembers = true)] class ProductRequestDelegate : NSObject, ISKProductsRequestDelegate, ISKRequestDelegate { + bool ignoreInvalidProducts; + public ProductRequestDelegate(bool ignoreInvalidProducts) + { + this.ignoreInvalidProducts = ignoreInvalidProducts; + } + readonly TaskCompletionSource> tcsResponse = new(); public Task> WaitForResponse() => @@ -638,12 +644,15 @@ class ProductRequestDelegate : NSObject, ISKProductsRequestDelegate, ISKRequestD public void ReceivedResponse(SKProductsRequest request, SKProductsResponse response) { - var invalidProduct = response.InvalidProducts; - if (invalidProduct?.Any() ?? false) - { - tcsResponse.TrySetException(new InAppBillingPurchaseException(PurchaseError.InvalidProduct, $"Invalid Product: {invalidProduct.First()}")); - return; - } + if (!ignoreInvalidProducts) + { + var invalidProducts = response.InvalidProducts; + if (invalidProducts?.Any() ?? false) + { + tcsResponse.TrySetException(new InAppBillingPurchaseException(PurchaseError.InvalidProduct, "Invalid products found when querying product list", invalidProducts)); + return; + } + } var product = response.Products; if (product != null) diff --git a/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs b/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs index b0c5f1e..9f06d30 100644 --- a/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs +++ b/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs @@ -31,6 +31,12 @@ public abstract class BaseInAppBilling : IInAppBilling, IDisposable /// public virtual bool IsConnected { get; set; } = true; + + /// + /// If you want to ignore invalid products when getting details + /// + public virtual bool IgnoreInvalidProducts { get; set; } = false; + /// /// Gets or sets if in testing mode /// diff --git a/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs b/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs index 6c1a71a..b4fe744 100644 --- a/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs +++ b/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs @@ -20,6 +20,12 @@ public interface IInAppBilling : IDisposable /// bool InTestingMode { get; set; } + + /// + /// If you want to ignore invalid products when getting details + /// + bool IgnoreInvalidProducts { get; set; } + /// /// Represenation of the storefront if available /// diff --git a/src/Plugin.InAppBilling/Shared/InAppBillingExceptions.shared.cs b/src/Plugin.InAppBilling/Shared/InAppBillingExceptions.shared.cs index b9ea9b8..39ce8c3 100644 --- a/src/Plugin.InAppBilling/Shared/InAppBillingExceptions.shared.cs +++ b/src/Plugin.InAppBilling/Shared/InAppBillingExceptions.shared.cs @@ -82,6 +82,9 @@ public class InAppBillingPurchaseException : Exception /// Type of error /// public PurchaseError PurchaseError { get; } + + public string[] Invalid { get; } + /// /// /// @@ -99,5 +102,16 @@ public class InAppBillingPurchaseException : Exception /// /// public InAppBillingPurchaseException(PurchaseError error, string message) : base(message) => PurchaseError = error; + + + /// + /// + /// + /// + public InAppBillingPurchaseException(PurchaseError error, string message, string[] invalid) : base(message) + { + PurchaseError = error; + Invalid = invalid; + } } }