From 2f6241a28d565c1a801d94bd5e638ac2accb9f20 Mon Sep 17 00:00:00 2001 From: Allan Ritchie Date: Wed, 8 May 2024 11:42:35 -0400 Subject: [PATCH 01/22] Add cancellation tokens to the interface (break everything) --- .../Shared/IInAppBilling.shared.cs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs b/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs index b4fe744..a546a35 100644 --- a/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs +++ b/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Plugin.InAppBilling @@ -37,27 +38,27 @@ public interface IInAppBilling : IDisposable /// /// /// if all were acknowledged/finalized - Task> FinalizePurchaseAsync(params string[] transactionIdentifier); + Task> FinalizePurchaseAsync(string[] transactionIdentifier, CancellationToken cancellationToken = default); /// /// Manually acknowledge/finalize a product id /// /// /// if all were acknowledged/finalized - Task> FinalizePurchaseOfProductAsync(params string[] productIds); + Task> FinalizePurchaseOfProductAsync(string[] productIds, CancellationToken cancellationToken = default); /// /// Connect to billing service /// /// If Success - Task ConnectAsync(bool enablePendingPurchases = true); + Task ConnectAsync(bool enablePendingPurchases = true, CancellationToken cancellationToken = default); /// /// Disconnect from the billing service /// /// Task to disconnect - Task DisconnectAsync(); + Task DisconnectAsync(CancellationToken cancellationToken = default); /// /// Get product information of a specific product @@ -65,14 +66,14 @@ public interface IInAppBilling : IDisposable /// Type of product offering /// Sku or Id of the product(s) /// List of products - Task> GetProductInfoAsync(ItemType itemType, params string[] productIds); + Task> GetProductInfoAsync(ItemType itemType, string[] productIds, CancellationToken cancellationToken = default); /// /// Get all current purchases for a specific product type. If you use verification and it fails for some purchase, it's not contained in the result. /// /// Type of product /// The current purchases - Task> GetPurchasesAsync(ItemType itemType); + Task> GetPurchasesAsync(ItemType itemType, CancellationToken cancellationToken = default); /// @@ -80,7 +81,7 @@ public interface IInAppBilling : IDisposable /// /// Type of product /// The current purchases - Task> GetPurchasesHistoryAsync(ItemType itemType); + Task> GetPurchasesHistoryAsync(ItemType itemType, CancellationToken cancellationToken = default); /// /// Purchase a specific product or subscription @@ -91,7 +92,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, string subOfferToken = null); + Task PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null, string subOfferToken = null, CancellationToken cancellationToken = default); /// /// (Android specific) Upgrade/Downgrade a previously purchased subscription @@ -101,7 +102,7 @@ public interface IInAppBilling : IDisposable /// Proration mode (1 - ImmediateWithTimeProration, 2 - ImmediateAndChargeProratedPrice, 3 - ImmediateWithoutProration, 4 - Deferred) /// Purchase details /// If an error occurs during processing - Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration); + Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration, CancellationToken cancellationToken = default); /// /// Consume a purchase with a purchase token. @@ -110,7 +111,7 @@ public interface IInAppBilling : IDisposable /// Original Purchase Token /// If consumed successful /// If an error occurs during processing - Task ConsumePurchaseAsync(string productId, string transactionIdentifier); + Task ConsumePurchaseAsync(string productId, string transactionIdentifier, CancellationToken cancellationToken = default); /// /// Get receipt data on iOS From 222714e64e0d9cc109f2b5a4b0e0b0408e997425 Mon Sep 17 00:00:00 2001 From: Allan Ritchie Date: Wed, 8 May 2024 11:53:29 -0400 Subject: [PATCH 02/22] Cancellation tokens for Apple --- src/Plugin.InAppBilling/InAppBilling.apple.cs | 149 ++++++++++-------- .../Shared/BaseInAppBilling.shared.cs | 23 +-- 2 files changed, 91 insertions(+), 81 deletions(-) diff --git a/src/Plugin.InAppBilling/InAppBilling.apple.cs b/src/Plugin.InAppBilling/InAppBilling.apple.cs index 4465904..3ac7f29 100644 --- a/src/Plugin.InAppBilling/InAppBilling.apple.cs +++ b/src/Plugin.InAppBilling/InAppBilling.apple.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Plugin.InAppBilling @@ -115,7 +116,7 @@ internal static bool HasFamilyShareable /// /// iOS: Displays a sheet that enables users to redeem subscription offer codes that you configure in App Store Connect. /// - public override void PresentCodeRedemption() + public override void PresentCodeRedemption() { #if __IOS__ && !__MACCATALYST__ if(HasFamilyShareable) @@ -150,7 +151,7 @@ public override void PresentCodeRedemption() public static Action OnPurchaseFailure { get; set; } = null; /// - /// + /// /// public static Func OnShouldAddStorePayment { get; set; } = null; @@ -183,10 +184,10 @@ void Init() /// Sku or Id of the product(s) /// Type of product offering /// - public async override Task> GetProductInfoAsync(ItemType itemType, params string[] productIds) + public async override Task> GetProductInfoAsync(ItemType itemType, string[] productIds, CancellationToken cancellationToken) { Init(); - var products = await GetProductAsync(productIds); + var products = await GetProductAsync(productIds, cancellationToken); return products.Select(p => new InAppBillingProduct { @@ -207,7 +208,7 @@ public async override Task> GetProductInfoAsync }); } - Task> GetProductAsync(string[] productId) + Task> GetProductAsync(string[] productId, CancellationToken cancellationToken) { var productIdentifiers = NSSet.MakeNSObjectSet(productId.Select(i => new NSString(i)).ToArray()); @@ -218,7 +219,8 @@ Task> GetProductAsync(string[] productId) { Delegate = productRequestDelegate // SKProductsRequestDelegate.ReceivedResponse }; - productsRequest.Start(); + using var _ = cancellationToken.Register(() => productsRequest.Cancel()); + productsRequest.Start(); return productRequestDelegate.WaitForResponse(); } @@ -228,10 +230,10 @@ Task> GetProductAsync(string[] productId) /// /// /// - public async override Task> GetPurchasesAsync(ItemType itemType) + public async override Task> GetPurchasesAsync(ItemType itemType, CancellationToken cancellationToken) { Init(); - var purchases = await RestoreAsync(); + var purchases = await RestoreAsync(cancellationToken); var comparer = new InAppBillingPurchaseComparer(); return purchases @@ -242,18 +244,14 @@ public async override Task> GetPurchasesAsync( - Task RestoreAsync() + Task RestoreAsync(CancellationToken cancellationToken) { var tcsTransaction = new TaskCompletionSource(); var allTransactions = new List(); - Action handler = null; - handler = new Action(transactions => + var handler = new Action(transactions => { - // Unsubscribe from future events - paymentObserver.TransactionsRestored -= handler; - if (transactions == null) { if (allTransactions.Count == 0) @@ -268,22 +266,29 @@ Task RestoreAsync() } }); + try + { + using var _ = cancellationToken.Register(() => tcsTransaction.TrySetCanceled()); + paymentObserver.TransactionsRestored += handler; - paymentObserver.TransactionsRestored += handler; - - foreach (var trans in SKPaymentQueue.DefaultQueue.Transactions) - { - var original = FindOriginalTransaction(trans); - if (original == null) - continue; + foreach (var trans in SKPaymentQueue.DefaultQueue.Transactions) + { + var original = FindOriginalTransaction(trans); + if (original == null) + continue; - allTransactions.Add(original); - } + allTransactions.Add(original); + } - // Start receiving restored transactions - SKPaymentQueue.DefaultQueue.RestoreCompletedTransactions(); + // Start receiving restored transactions + SKPaymentQueue.DefaultQueue.RestoreCompletedTransactions(); - return tcsTransaction.Task; + return tcsTransaction.Task; + } + finally + { + paymentObserver.TransactionsRestored -= handler; + } } @@ -315,10 +320,10 @@ 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, string subOfferToken = null) + public async override Task PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null, string subOfferToken = null, CancellationToken cancellationToken) { Init(); - var p = await PurchaseAsync(productId, itemType, obfuscatedAccountId); + var p = await PurchaseAsync(productId, itemType, obfuscatedAccountId, cancellationToken); var reference = new DateTime(2001, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); @@ -342,12 +347,11 @@ public async override Task PurchaseAsync(string productId, } - async Task PurchaseAsync(string productId, ItemType itemType, string applicationUserName) + async Task PurchaseAsync(string productId, ItemType itemType, string applicationUserName, CancellationToken cancellationToken) { var tcsTransaction = new TaskCompletionSource(); - Action handler = null; - handler = new Action((tran, success) => + var handler = new Action((tran, success) => { if (tran?.Payment == null) return; @@ -356,9 +360,6 @@ async Task PurchaseAsync(string productId, ItemType itemTy if (productId != tran.Payment.ProductIdentifier) return; - // Unsubscribe from future events - paymentObserver.TransactionCompleted -= handler; - if (success) { tcsTransaction.TrySetResult(tran); @@ -383,8 +384,8 @@ async Task PurchaseAsync(string productId, ItemType itemTy error = PurchaseError.ItemUnavailable; break; case (int)SKError.Unknown: - try - { + try + { var underlyingError = tran?.Error?.UserInfo?["NSUnderlyingError"] as NSError; error = underlyingError?.Code == 3038 ? PurchaseError.AppleTermsConditionsChanged : PurchaseError.GeneralError; } @@ -401,37 +402,45 @@ async Task PurchaseAsync(string productId, ItemType itemTy tcsTransaction.TrySetException(new InAppBillingPurchaseException(error, description)); }); - - paymentObserver.TransactionCompleted += handler; - var products = await GetProductAsync(new[] { productId }); - var product = products?.FirstOrDefault(); - if (product == null) - throw new InAppBillingPurchaseException(PurchaseError.InvalidProduct); - - if (string.IsNullOrWhiteSpace(applicationUserName)) + try { - var payment = SKPayment.CreateFrom(product); - //var payment = SKPayment.CreateFrom((SKProduct)SKProduct.FromObject(new NSString(productId))); - - SKPaymentQueue.DefaultQueue.AddPayment(payment); + using var _ = cancellationToken.Register(() => tcsTransaction.TrySetCanceled()); + paymentObserver.TransactionCompleted += handler; + + var products = await GetProductAsync(new[] { productId }, cancellationToken); + var product = products?.FirstOrDefault(); + if (product == null) + throw new InAppBillingPurchaseException(PurchaseError.InvalidProduct); + + if (string.IsNullOrWhiteSpace(applicationUserName)) + { + var payment = SKPayment.CreateFrom(product); + //var payment = SKPayment.CreateFrom((SKProduct)SKProduct.FromObject(new NSString(productId))); + + SKPaymentQueue.DefaultQueue.AddPayment(payment); + } + else + { + var payment = SKMutablePayment.PaymentWithProduct(product); + payment.ApplicationUsername = applicationUserName; + + SKPaymentQueue.DefaultQueue.AddPayment(payment); + } + + return await tcsTransaction.Task; } - else + finally { - var payment = SKMutablePayment.PaymentWithProduct(product); - payment.ApplicationUsername = applicationUserName; - - SKPaymentQueue.DefaultQueue.AddPayment(payment); + paymentObserver.TransactionCompleted -= handler; } - - return await tcsTransaction.Task; } /// /// (iOS not supported) Apple store manages upgrades natively when subscriptions of the same group are purchased. /// /// iOS not supported - public override Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration) => + public override Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration, CancellationToken cancellationToken) => throw new NotImplementedException("iOS not supported. Apple store manages upgrades natively when subscriptions of the same group are purchased."); @@ -460,23 +469,23 @@ public override string ReceiptData /// Original Purchase Token /// If consumed successful /// If an error occurs during processing - public override async Task ConsumePurchaseAsync(string productId, string transactionIdentifier) + public override async Task ConsumePurchaseAsync(string productId, string transactionIdentifier, CancellationToken cancellationToken) { - var items = await FinalizePurchaseAsync(transactionIdentifier); - var item = items.FirstOrDefault(); + var items = await FinalizePurchaseAsync(new [] { transactionIdentifier }, cancellationToken); + var item = items.FirstOrDefault(); return item.Success; } /// - /// + /// /// /// /// - public override async Task> FinalizePurchaseOfProductAsync(params string[] productIds) + public override async Task> FinalizePurchaseOfProductAsync(string[] productIds, CancellationToken cancellationToken) { - var purchases = await RestoreAsync(); + var purchases = await RestoreAsync(cancellationToken); var items = new List<(string Id, bool Success)>(); @@ -486,7 +495,7 @@ public override async Task ConsumePurchaseAsync(string productId, string t return items; } - + foreach (var t in productIds) { if (string.IsNullOrWhiteSpace(t)) @@ -509,7 +518,7 @@ public override async Task ConsumePurchaseAsync(string productId, string t { try - { + { SKPaymentQueue.DefaultQueue.FinishTransaction(transaction); } catch (Exception ex) @@ -533,10 +542,10 @@ public override async Task ConsumePurchaseAsync(string productId, string t /// /// /// - public async override Task> FinalizePurchaseAsync(params string[] transactionIdentifier) + public async override Task> FinalizePurchaseAsync(string[] transactionIdentifier, CancellationToken cancellationToken) { - var purchases = await RestoreAsync(); - + var purchases = await RestoreAsync(cancellationToken); + var items = new List<(string Id, bool Success)>(); @@ -683,7 +692,7 @@ public PaymentObserver(Action onPurchaseSuccess, Action + public override bool ShouldAddStorePayment(SKPaymentQueue queue, SKPayment payment, SKProduct product) => onShouldAddStorePayment?.Invoke(queue, payment, product) ?? false; public override void UpdatedTransactions(SKPaymentQueue queue, SKPaymentTransaction[] transactions) @@ -894,10 +903,10 @@ public static InAppBillingProductDiscount ToProductDiscount(this SKProductDiscou { if (!InAppBillingImplementation.HasIntroductoryOffer) return null; - + if (pd == null) return null; - + var discount = new InAppBillingProductDiscount { diff --git a/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs b/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs index 9f06d30..cc72d5f 100644 --- a/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs +++ b/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Plugin.InAppBilling @@ -46,13 +47,13 @@ public abstract class BaseInAppBilling : IInAppBilling, IDisposable /// Connect to billing service /// /// If Success - public virtual Task ConnectAsync(bool enablePendingPurchases = true) => Task.FromResult(true); + public virtual Task ConnectAsync(bool enablePendingPurchases = true, CancellationToken cancellationToken = default) => Task.FromResult(true); /// /// Disconnect from the billing service /// /// Task to disconnect - public virtual Task DisconnectAsync() => Task.CompletedTask; + public virtual Task DisconnectAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; /// /// Get product information of a specific product @@ -60,7 +61,7 @@ public abstract class BaseInAppBilling : IInAppBilling, IDisposable /// Type of product offering /// Sku or Id of the product(s) /// List of products - public abstract Task> GetProductInfoAsync(ItemType itemType, params string[] productIds); + public abstract Task> GetProductInfoAsync(ItemType itemType, string[] productIds, CancellationToken cancellationToken = default); @@ -69,7 +70,7 @@ public abstract class BaseInAppBilling : IInAppBilling, IDisposable /// /// Type of product /// The current purchases - public abstract Task> GetPurchasesAsync(ItemType itemType); + public abstract Task> GetPurchasesAsync(ItemType itemType, CancellationToken cancellationToken = default); @@ -78,7 +79,7 @@ public abstract class BaseInAppBilling : IInAppBilling, IDisposable /// /// Type of product /// The current purchases - public virtual Task> GetPurchasesHistoryAsync(ItemType itemType) => + public virtual Task> GetPurchasesHistoryAsync(ItemType itemType, CancellationToken cancellationToken = default) => Task.FromResult>(new List()); /// @@ -90,7 +91,7 @@ public virtual Task> GetPurchasesHistoryAsync( /// 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, string subOfferToken = null); + public abstract Task PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null, string subOfferToken = null, CancellationToken cancellationToken = default); /// /// (Android specific) Upgrade/Downgrade a previously purchased subscription @@ -99,7 +100,7 @@ public virtual Task> GetPurchasesHistoryAsync( /// Purchase token of original subscription (can not be null) /// Proration mode /// Purchase details - public abstract Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration); + public abstract Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration, CancellationToken cancellationToken = default); /// /// Consume a purchase with a purchase token. @@ -108,7 +109,7 @@ public virtual Task> GetPurchasesHistoryAsync( /// Original Purchase Token /// If consumed successful /// If an error occurs during processing - public abstract Task ConsumePurchaseAsync(string productId, string transactionIdentifier); + public abstract Task ConsumePurchaseAsync(string productId, string transactionIdentifier, CancellationToken cancellationToken = default); /// /// Dispose of class and parent classes @@ -149,14 +150,14 @@ public virtual void Dispose(bool disposing) /// /// /// - public virtual Task> FinalizePurchaseAsync(params string[] transactionIdentifier) => Task.FromResult(new List<(string Id, bool Success)>().AsEnumerable()); + public virtual Task> FinalizePurchaseAsync(string[] transactionIdentifier, CancellationToken cancellationToken = default) => Task.FromResult(new List<(string Id, bool Success)>().AsEnumerable()); /// - /// + /// /// /// /// - public virtual Task> FinalizePurchaseOfProductAsync(params string[] productIds) => Task.FromResult(new List<(string Id, bool Success)>().AsEnumerable()); + public virtual Task> FinalizePurchaseOfProductAsync(params string[] productIds, CancellationToken cancellationToken = default) => Task.FromResult(new List<(string Id, bool Success)>().AsEnumerable()); /// /// iOS: Displays a sheet that enables users to redeem subscription offer codes that you configure in App Store Connect. From d9d95148aadfb56142c917ba3667881c64f3d59d Mon Sep 17 00:00:00 2001 From: Allan Ritchie Date: Wed, 8 May 2024 12:14:36 -0400 Subject: [PATCH 03/22] Add Android with cancellation tokens --- .../InAppBilling.android.cs | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/Plugin.InAppBilling/InAppBilling.android.cs b/src/Plugin.InAppBilling/InAppBilling.android.cs index 3d79c1b..e11a5ef 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Android.App; 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 @@ -45,6 +47,18 @@ public InAppBillingImplementation() } + void AssertPurchaseTransactionReady() + { + if (BillingClient == null || !IsConnected) + { + throw new InAppBillingPurchaseException(PurchaseError.ServiceUnavailable, "You are not connected to the Google Play App store."); + } + if (tcsPurchase?.Task != null && !tcsPurchase.Task.IsCompleted) + { + throw new InvalidOperationException("Purchase task is already running. Please wait or cancel previous request"); + } + } + BillingClient BillingClient { get; set; } BillingClient.Builder BillingClientBuilder { get; set; } /// @@ -57,7 +71,7 @@ public InAppBillingImplementation() /// Connect to billing service /// /// If Success - public override Task ConnectAsync(bool enablePendingPurchases = true) + public override Task ConnectAsync(bool enablePendingPurchases = true, CancellationToken cancellationToken) { tcsPurchase?.TrySetCanceled(); tcsPurchase = null; @@ -65,6 +79,7 @@ public override Task ConnectAsync(bool enablePendingPurchases = true) tcsConnect?.TrySetCanceled(); tcsConnect = new TaskCompletionSource(); + using var _ = cancellationToken.Register(() => tcsConnect.TrySetCanceled()); BillingClientBuilder = NewBuilder(Context); BillingClientBuilder.SetListener(OnPurchasesUpdated); if (enablePendingPurchases) @@ -73,6 +88,7 @@ public override Task ConnectAsync(bool enablePendingPurchases = true) BillingClient = BillingClientBuilder.Build(); BillingClient.StartConnection(OnSetupFinished, OnDisconnected); + // TODO: stop trying return tcsConnect.Task; @@ -103,7 +119,7 @@ public void OnPurchasesUpdated(BillingResult billingResult, IList /// Task to disconnect - public override Task DisconnectAsync() + public override Task DisconnectAsync(CancellationToken cancellationToken) { try { @@ -134,7 +150,7 @@ public override Task DisconnectAsync() /// Sku or Id of the product /// Type of product offering /// - public async override Task> GetProductInfoAsync(ItemType itemType, params string[] productIds) + public async override Task> GetProductInfoAsync(ItemType itemType, string[] productIds, CancellationToken cancellationToken) { if (BillingClient == null || !IsConnected) { @@ -166,12 +182,11 @@ public async override Task> GetProductInfoAsync var skuDetailsResult = await BillingClient.QueryProductDetailsAsync(skuDetailsParams.Build()); ParseBillingResult(skuDetailsResult?.Result, IgnoreInvalidProducts); - return skuDetailsResult.ProductDetails.Select(product => product.ToIAPProduct()); } - public override async Task> GetPurchasesAsync(ItemType itemType) + public override async Task> GetPurchasesAsync(ItemType itemType, CancellationToken cancellationToken) { if (BillingClient == null) throw new InAppBillingPurchaseException(PurchaseError.ServiceUnavailable, "You are not connected to the Google Play App store."); @@ -196,7 +211,7 @@ public override async Task> GetPurchasesAsync( /// /// Type of product /// The current purchases - public override async Task> GetPurchasesHistoryAsync(ItemType itemType) + public override async Task> GetPurchasesHistoryAsync(ItemType itemType, CancellationToken cancellationToken) { if (BillingClient == null) throw new InAppBillingPurchaseException(PurchaseError.ServiceUnavailable, "You are not connected to the Google Play App store."); @@ -222,34 +237,22 @@ 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, CancellationToken cancellationToken) { - if (BillingClient == null || !IsConnected) - { - throw new InAppBillingPurchaseException(PurchaseError.ServiceUnavailable, "You are not connected to the Google Play App store."); - } // If we have a current task and it is not completed then return null. // you can't try to purchase twice. - if (tcsPurchase?.Task != null && !tcsPurchase.Task.IsCompleted) - { - return null; - } + AssertPurchaseTransactionReady(); - var purchase = await UpgradePurchasedSubscriptionInternalAsync(newProductId, purchaseTokenOfOriginalSubscription, prorationMode); + var purchase = await UpgradePurchasedSubscriptionInternalAsync(newProductId, purchaseTokenOfOriginalSubscription, prorationMode, cancellationToken); return purchase; } - async Task UpgradePurchasedSubscriptionInternalAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode) + async Task UpgradePurchasedSubscriptionInternalAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode, CancellationToken cancellationToken) { var itemType = ProductType.Subs; - if (tcsPurchase?.Task != null && !tcsPurchase.Task.IsCompleted) - { - return null; - } - var productList = QueryProductDetailsParams.Product.NewBuilder() .SetProductType(itemType) .SetProductId(newProductId) @@ -285,6 +288,7 @@ async Task UpgradePurchasedSubscriptionInternalAsync(strin .Build(); tcsPurchase = new TaskCompletionSource<(BillingResult billingResult, IList purchases)>(); + using var _ = cancellationToken.Register(() => tcsPurchase.TrySetCanceled()); var responseCode = BillingClient.LaunchBillingFlow(Activity, flowParams); ParseBillingResult(responseCode); @@ -298,7 +302,7 @@ async Task UpgradePurchasedSubscriptionInternalAsync(strin //for some reason the data didn't come back if (androidPurchase == null) { - var purchases = await GetPurchasesAsync(itemType == ProductType.Inapp ? ItemType.InAppPurchase : ItemType.Subscription); + var purchases = await GetPurchasesAsync(itemType == ProductType.Inapp ? ItemType.InAppPurchase : ItemType.Subscription, cancellationToken); return purchases.FirstOrDefault(p => p.ProductId == newProductId); } @@ -313,7 +317,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, string subOfferToken = null) + public async override Task PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null, string subOfferToken = null, CancellationToken cancellationToken = default) { if (BillingClient == null || !IsConnected) { @@ -322,30 +326,27 @@ public async override Task PurchaseAsync(string productId, // If we have a current task and it is not completed then return null. // you can't try to purchase twice. - if (tcsPurchase?.Task != null && !tcsPurchase.Task.IsCompleted) - { - return null; - } - - if(!string.IsNullOrWhiteSpace(obfuscatedProfileId) && string.IsNullOrWhiteSpace(obfuscatedAccountId)) + AssertPurchaseTransactionReady(); + + 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) { case ItemType.InAppPurchase: case ItemType.InAppPurchaseConsumable: - return await PurchaseAsync(productId, ProductType.Inapp, obfuscatedAccountId, obfuscatedProfileId); + return await PurchaseAsync(productId, ProductType.Inapp, obfuscatedAccountId, obfuscatedProfileId, null, cancellationToken); case ItemType.Subscription: var result = BillingClient.IsFeatureSupported(FeatureType.Subscriptions); ParseBillingResult(result); - return await PurchaseAsync(productId, ProductType.Subs, obfuscatedAccountId, obfuscatedProfileId, subOfferToken); + return await PurchaseAsync(productId, ProductType.Subs, obfuscatedAccountId, obfuscatedProfileId, subOfferToken, cancellationToken); } return null; } - async Task PurchaseAsync(string productSku, string itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null, string subOfferToken = null) + async Task PurchaseAsync(string productSku, string itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null, string subOfferToken = null, CancellationToken cancellationToken = default) { var productList = QueryProductDetailsParams.Product.NewBuilder() @@ -383,9 +384,8 @@ async Task PurchaseAsync(string productSku, string itemTyp var flowParams = billingFlowParams.Build(); - tcsPurchase = new TaskCompletionSource<(BillingResult billingResult, IList purchases)>(); - + var _ = cancellationToken.Register(() => tcsPurchase.TrySetCanceled()); var responseCode = BillingClient.LaunchBillingFlow(Activity, flowParams); @@ -400,7 +400,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 == ProductType.Inapp ? ItemType.InAppPurchase : ItemType.Subscription); + var purchases = await GetPurchasesAsync(itemType == ProductType.Inapp ? ItemType.InAppPurchase : ItemType.Subscription, cancellationToken); return purchases.FirstOrDefault(p => p.ProductId == productSku); } @@ -408,7 +408,7 @@ async Task PurchaseAsync(string productSku, string itemTyp } - public async override Task> FinalizePurchaseAsync(params string[] transactionIdentifier) + public async override Task> FinalizePurchaseAsync(string[] transactionIdentifier, CancellationToken cancellationToken) { if (BillingClient == null || !IsConnected) throw new InAppBillingPurchaseException(PurchaseError.ServiceUnavailable, "You are not connected to the Google Play App store."); @@ -438,7 +438,7 @@ async Task PurchaseAsync(string productSku, string itemTyp /// Id or Sku of product /// Original Purchase Token /// If consumed successful - public override async Task ConsumePurchaseAsync(string productId, string transactionIdentifier) + public override async Task ConsumePurchaseAsync(string productId, string transactionIdentifier, CancellationToken cancellationToken) { if (BillingClient == null || !IsConnected) { From a2dcb664fa6a058f2f3509460971c3b111a109ba Mon Sep 17 00:00:00 2001 From: Allan Ritchie Date: Wed, 8 May 2024 12:21:00 -0400 Subject: [PATCH 04/22] Windows signature updates --- src/Plugin.InAppBilling/InAppBilling.uwp.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Plugin.InAppBilling/InAppBilling.uwp.cs b/src/Plugin.InAppBilling/InAppBilling.uwp.cs index 33d0110..5cca442 100644 --- a/src/Plugin.InAppBilling/InAppBilling.uwp.cs +++ b/src/Plugin.InAppBilling/InAppBilling.uwp.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Xml; using Windows.ApplicationModel.Store; @@ -32,7 +33,7 @@ public InAppBillingImplementation() /// Type of item /// Product Ids /// - public async override Task> GetProductInfoAsync(ItemType itemType, params string[] productIds) + public async override Task> GetProductInfoAsync(ItemType itemType, string[] productIds, CancellationToken cancellationToken) { // Get list of products from store or simulator var listingInformation = await CurrentAppMock.LoadListingInformationAsync(InTestingMode); @@ -53,17 +54,17 @@ public async override Task> GetProductInfoAsync Description = product.Description, ProductId = product.ProductId, LocalizedPrice = product.FormattedPrice, - WindowsExtras = new InAppBillingProductWindowsExtras + WindowsExtras = new InAppBillingProductWindowsExtras { FormattedBasePrice = product.FormattedBasePrice, ImageUri = product.ImageUri, - IsOnSale = product.IsOnSale, + IsOnSale = product.IsOnSale, SaleEndDate = product.SaleEndDate, Tag = product.Tag, IsConsumable = product.ProductType == ProductType.Consumable, IsDurable = product.ProductType == ProductType.Durable, Keywords = product.Keywords - } + } //CurrencyCode = product.CurrencyCode // Does not work at the moment, as UWP throws an InvalidCastException when getting CurrencyCode }); } @@ -76,7 +77,7 @@ public async override Task> GetProductInfoAsync /// /// /// - public async override Task> GetPurchasesAsync(ItemType itemType) + public async override Task> GetPurchasesAsync(ItemType itemType, CancellationToken cancellationToken) { // Get list of product receipts from store or simulator var xmlReceipt = await CurrentAppMock.GetAppReceiptAsync(InTestingMode); @@ -94,7 +95,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, string subOfferToken = null) + public async override Task PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null, string subOfferToken = null, CancellationToken cancellationToken) { // Get purchase result from store or simulator var purchaseResult = await CurrentAppMock.RequestProductPurchaseAsync(InTestingMode, productId); @@ -108,14 +109,14 @@ public async override Task PurchaseAsync(string productId, // Transform it to InAppBillingPurchase return purchaseResult.ReceiptXml.ToInAppBillingPurchase(purchaseResult.Status).FirstOrDefault(); - + } /// /// (UWP not supported) Upgrade/Downgrade/Change a previously purchased subscription /// /// UWP not supported - public override Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration) => + public override Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration, CancellationToken cancellationToken) => throw new NotImplementedException("UWP not supported."); /// @@ -125,7 +126,7 @@ public override Task UpgradePurchasedSubscriptionAsync(str /// Original Purchase Token /// If consumed successful /// If an error occures during processing - public async override Task ConsumePurchaseAsync(string productId, string transactionIdentifier) + public async override Task ConsumePurchaseAsync(string productId, string transactionIdentifier, CancellationToken cancellationToken) { var result = await CurrentAppMock.ReportConsumableFulfillmentAsync(InTestingMode, productId, new Guid(transactionIdentifier)); return result switch From caac0eabd9071d6ee9839640f98202f287274935 Mon Sep 17 00:00:00 2001 From: Allan Ritchie Date: Thu, 23 May 2024 21:40:58 -0400 Subject: [PATCH 05/22] .NET 8ify --- .gitignore | 1 + src/.idea/.idea.InAppBilling/.idea/.name | 1 + .../.idea.InAppBilling/.idea/indexLayout.xml | 8 + .../.idea/projectSettingsUpdater.xml | 6 + src/.idea/.idea.InAppBilling/.idea/vcs.xml | 6 + src/InAppBilling.sln | 435 - .../InAppBillingMauiTest/AppShell.xaml | 2 +- .../InAppBillingMauiTest.csproj | 99 +- .../InAppBillingMauiTest/MainPage.xaml | 58 +- .../InAppBillingMauiTest/MainPage.xaml.cs | 144 +- .../Assets/AboutAssets.txt | 19 - .../InAppBillingTests.Android.csproj | 97 - .../InAppBillingTests.Android/MainActivity.cs | 26 - .../Properties/AndroidManifest.xml | 5 - .../Properties/AssemblyInfo.cs | 34 - .../Resources/AboutResources.txt | 50 - .../Resources/Resource.designer.cs | 29656 ---------------- .../Resources/layout/Tabbar.axml | 11 - .../Resources/layout/Toolbar.axml | 9 - .../Resources/mipmap-anydpi-v26/icon.xml | 5 - .../mipmap-anydpi-v26/icon_round.xml | 5 - .../Resources/mipmap-hdpi/Icon.png | Bin 4754 -> 0 bytes .../mipmap-hdpi/launcher_foreground.png | Bin 11695 -> 0 bytes .../Resources/mipmap-mdpi/icon.png | Bin 2807 -> 0 bytes .../mipmap-mdpi/launcher_foreground.png | Bin 6439 -> 0 bytes .../Resources/mipmap-xhdpi/Icon.png | Bin 7028 -> 0 bytes .../mipmap-xhdpi/launcher_foreground.png | Bin 17898 -> 0 bytes .../Resources/mipmap-xxhdpi/Icon.png | Bin 12827 -> 0 bytes .../mipmap-xxhdpi/launcher_foreground.png | Bin 33484 -> 0 bytes .../Resources/mipmap-xxxhdpi/Icon.png | Bin 19380 -> 0 bytes .../mipmap-xxxhdpi/launcher_foreground.png | Bin 52285 -> 0 bytes .../Resources/values/colors.xml | 7 - .../Resources/values/styles.xml | 30 - .../InAppBillingTests.Mac/AppDelegate.cs | 34 - .../AppIcon.appiconset/AppIcon-128.png | Bin 8125 -> 0 bytes .../AppIcon.appiconset/AppIcon-128@2x.png | Bin 20798 -> 0 bytes .../AppIcon.appiconset/AppIcon-16.png | Bin 711 -> 0 bytes .../AppIcon.appiconset/AppIcon-16@2x.png | Bin 1484 -> 0 bytes .../AppIcon.appiconset/AppIcon-256.png | Bin 20798 -> 0 bytes .../AppIcon.appiconset/AppIcon-256@2x.png | Bin 59335 -> 0 bytes .../AppIcon.appiconset/AppIcon-32.png | Bin 1484 -> 0 bytes .../AppIcon.appiconset/AppIcon-32@2x.png | Bin 3428 -> 0 bytes .../AppIcon.appiconset/AppIcon-512.png | Bin 59335 -> 0 bytes .../AppIcon.appiconset/AppIcon-512@2x.png | Bin 177632 -> 0 bytes .../AppIcon.appiconset/Contents.json | 68 - .../Assets.xcassets/Contents.json | 6 - .../InAppBillingTests.Mac/Entitlements.plist | 6 - .../InAppBillingTests.Mac.csproj | 100 - .../InAppBillingTests.Mac/Info.plist | 32 - .../InAppBillingTests.Mac/Main.cs | 14 - .../InAppBillingTests.Mac/Main.storyboard | 713 - .../InAppBillingTests.Mac/ViewController.cs | 34 - .../ViewController.designer.cs | 18 - .../InAppBillingTests.UWP/App.xaml | 8 - .../InAppBillingTests.UWP/App.xaml.cs | 101 - .../Assets/LargeTile.scale-100.png | Bin 6143 -> 0 bytes .../Assets/LargeTile.scale-200.png | Bin 13916 -> 0 bytes .../Assets/LargeTile.scale-400.png | Bin 31561 -> 0 bytes .../Assets/SmallTile.scale-100.png | Bin 1218 -> 0 bytes .../Assets/SmallTile.scale-200.png | Bin 2536 -> 0 bytes .../Assets/SmallTile.scale-400.png | Bin 5566 -> 0 bytes .../Assets/SplashScreen.scale-100.png | Bin 6555 -> 0 bytes .../Assets/SplashScreen.scale-200.png | Bin 15240 -> 0 bytes .../Assets/SplashScreen.scale-400.png | Bin 39781 -> 0 bytes .../Assets/Square150x150Logo.scale-100.png | Bin 2772 -> 0 bytes .../Assets/Square150x150Logo.scale-200.png | Bin 5904 -> 0 bytes .../Assets/Square150x150Logo.scale-400.png | Bin 13344 -> 0 bytes ...x44Logo.altform-unplated_targetsize-16.png | Bin 394 -> 0 bytes ...44Logo.altform-unplated_targetsize-256.png | Bin 9693 -> 0 bytes ...x44Logo.altform-unplated_targetsize-48.png | Bin 1245 -> 0 bytes .../Assets/Square44x44Logo.scale-100.png | Bin 1141 -> 0 bytes .../Assets/Square44x44Logo.scale-200.png | Bin 2468 -> 0 bytes .../Assets/Square44x44Logo.scale-400.png | Bin 4740 -> 0 bytes .../Assets/Square44x44Logo.targetsize-16.png | Bin 394 -> 0 bytes .../Assets/Square44x44Logo.targetsize-256.png | Bin 9693 -> 0 bytes .../Assets/Square44x44Logo.targetsize-48.png | Bin 1245 -> 0 bytes .../Assets/StoreLogo.backup.png | Bin 392 -> 0 bytes .../Assets/StoreLogo.scale-100.png | Bin 836 -> 0 bytes .../Assets/StoreLogo.scale-200.png | Bin 1742 -> 0 bytes .../Assets/StoreLogo.scale-400.png | Bin 3654 -> 0 bytes .../Assets/Wide310x150Logo.scale-100.png | Bin 2988 -> 0 bytes .../Assets/Wide310x150Logo.scale-200.png | Bin 6555 -> 0 bytes .../Assets/Wide310x150Logo.scale-400.png | Bin 15240 -> 0 bytes .../InAppBillingTests.UWP.csproj | 162 - .../InAppBillingTests.UWP/MainPage.xaml | 15 - .../InAppBillingTests.UWP/MainPage.xaml.cs | 27 - .../Package.appxmanifest | 55 - .../Properties/AssemblyInfo.cs | 29 - .../Properties/Default.rd.xml | 31 - .../InAppBillingTests.iOS/AppDelegate.cs | 46 - .../AppIcon.appiconset/Contents.json | 117 - .../AppIcon.appiconset/Icon1024.png | Bin 70429 -> 0 bytes .../AppIcon.appiconset/Icon120.png | Bin 3773 -> 0 bytes .../AppIcon.appiconset/Icon152.png | Bin 4750 -> 0 bytes .../AppIcon.appiconset/Icon167.png | Bin 4692 -> 0 bytes .../AppIcon.appiconset/Icon180.png | Bin 5192 -> 0 bytes .../AppIcon.appiconset/Icon20.png | Bin 1313 -> 0 bytes .../AppIcon.appiconset/Icon29.png | Bin 845 -> 0 bytes .../AppIcon.appiconset/Icon40.png | Bin 1101 -> 0 bytes .../AppIcon.appiconset/Icon58.png | Bin 1761 -> 0 bytes .../AppIcon.appiconset/Icon60.png | Bin 2537 -> 0 bytes .../AppIcon.appiconset/Icon76.png | Bin 2332 -> 0 bytes .../AppIcon.appiconset/Icon80.png | Bin 2454 -> 0 bytes .../AppIcon.appiconset/Icon87.png | Bin 2758 -> 0 bytes .../InAppBillingTests.iOS/Entitlements.plist | 7 - .../InAppBillingTests.iOS.csproj | 163 - .../InAppBillingTests.iOS/Info.plist | 38 - .../InAppBillingTests.iOS/Main.cs | 20 - .../Properties/AssemblyInfo.cs | 36 - .../Resources/Default-568h@2x.png | Bin 8884 -> 0 bytes .../Resources/Default-Portrait.png | Bin 10710 -> 0 bytes .../Resources/Default-Portrait@2x.png | Bin 34540 -> 0 bytes .../Resources/Default.png | Bin 7243 -> 0 bytes .../Resources/Default@2x.png | Bin 8368 -> 0 bytes .../Resources/LaunchScreen.storyboard | 39 - .../InAppBillingTests/App.xaml | 8 - .../InAppBillingTests/App.xaml.cs | 32 - .../InAppBillingTests.csproj | 20 - .../InAppBillingTests/MainPage.xaml | 24 - .../InAppBillingTests/MainPage.xaml.cs | 139 - .../CrossInAppBilling.shared.cs | 13 +- .../InAppBilling.android.cs | 4 +- src/Plugin.InAppBilling/InAppBilling.apple.cs | 4 +- .../Plugin.InAppBilling.csproj | 107 +- .../Shared/BaseInAppBilling.shared.cs | 2 +- 125 files changed, 252 insertions(+), 32734 deletions(-) create mode 100644 src/.idea/.idea.InAppBilling/.idea/.name create mode 100644 src/.idea/.idea.InAppBilling/.idea/indexLayout.xml create mode 100644 src/.idea/.idea.InAppBilling/.idea/projectSettingsUpdater.xml create mode 100644 src/.idea/.idea.InAppBilling/.idea/vcs.xml delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Assets/AboutAssets.txt delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/InAppBillingTests.Android.csproj delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/MainActivity.cs delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Properties/AndroidManifest.xml delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Properties/AssemblyInfo.cs delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Resources/AboutResources.txt delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Resources/Resource.designer.cs delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Resources/layout/Tabbar.axml delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Resources/layout/Toolbar.axml delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Resources/mipmap-anydpi-v26/icon.xml delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Resources/mipmap-anydpi-v26/icon_round.xml delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Resources/mipmap-hdpi/Icon.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Resources/mipmap-hdpi/launcher_foreground.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Resources/mipmap-mdpi/icon.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Resources/mipmap-mdpi/launcher_foreground.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Resources/mipmap-xhdpi/Icon.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Resources/mipmap-xhdpi/launcher_foreground.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Resources/mipmap-xxhdpi/Icon.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Resources/mipmap-xxhdpi/launcher_foreground.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Resources/mipmap-xxxhdpi/Icon.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Resources/mipmap-xxxhdpi/launcher_foreground.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Resources/values/colors.xml delete mode 100644 src/InAppBillingTests/InAppBillingTests.Android/Resources/values/styles.xml delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/AppDelegate.cs delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-128@2x.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-16@2x.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-256@2x.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-32@2x.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/Assets.xcassets/Contents.json delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/Entitlements.plist delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/InAppBillingTests.Mac.csproj delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/Info.plist delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/Main.cs delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/Main.storyboard delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/ViewController.cs delete mode 100644 src/InAppBillingTests/InAppBillingTests.Mac/ViewController.designer.cs delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/App.xaml delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/App.xaml.cs delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/LargeTile.scale-100.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/LargeTile.scale-200.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/LargeTile.scale-400.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/SmallTile.scale-100.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/SmallTile.scale-200.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/SmallTile.scale-400.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/SplashScreen.scale-100.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/SplashScreen.scale-200.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/SplashScreen.scale-400.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/Square150x150Logo.scale-100.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/Square150x150Logo.scale-200.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/Square150x150Logo.scale-400.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/Square44x44Logo.altform-unplated_targetsize-16.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/Square44x44Logo.altform-unplated_targetsize-256.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/Square44x44Logo.altform-unplated_targetsize-48.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/Square44x44Logo.scale-100.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/Square44x44Logo.scale-200.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/Square44x44Logo.scale-400.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/Square44x44Logo.targetsize-16.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/Square44x44Logo.targetsize-256.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/Square44x44Logo.targetsize-48.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/StoreLogo.backup.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/StoreLogo.scale-100.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/StoreLogo.scale-200.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/StoreLogo.scale-400.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/Wide310x150Logo.scale-100.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/Wide310x150Logo.scale-200.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Assets/Wide310x150Logo.scale-400.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/InAppBillingTests.UWP.csproj delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/MainPage.xaml delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/MainPage.xaml.cs delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Package.appxmanifest delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Properties/AssemblyInfo.cs delete mode 100644 src/InAppBillingTests/InAppBillingTests.UWP/Properties/Default.rd.xml delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/AppDelegate.cs delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Assets.xcassets/AppIcon.appiconset/Icon1024.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Assets.xcassets/AppIcon.appiconset/Icon120.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Assets.xcassets/AppIcon.appiconset/Icon152.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Assets.xcassets/AppIcon.appiconset/Icon167.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Assets.xcassets/AppIcon.appiconset/Icon180.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Assets.xcassets/AppIcon.appiconset/Icon20.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Assets.xcassets/AppIcon.appiconset/Icon29.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Assets.xcassets/AppIcon.appiconset/Icon40.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Assets.xcassets/AppIcon.appiconset/Icon58.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Assets.xcassets/AppIcon.appiconset/Icon60.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Assets.xcassets/AppIcon.appiconset/Icon76.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Assets.xcassets/AppIcon.appiconset/Icon80.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Assets.xcassets/AppIcon.appiconset/Icon87.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Entitlements.plist delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/InAppBillingTests.iOS.csproj delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Info.plist delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Main.cs delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Properties/AssemblyInfo.cs delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Resources/Default-568h@2x.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Resources/Default-Portrait.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Resources/Default-Portrait@2x.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Resources/Default.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Resources/Default@2x.png delete mode 100644 src/InAppBillingTests/InAppBillingTests.iOS/Resources/LaunchScreen.storyboard delete mode 100644 src/InAppBillingTests/InAppBillingTests/App.xaml delete mode 100644 src/InAppBillingTests/InAppBillingTests/App.xaml.cs delete mode 100644 src/InAppBillingTests/InAppBillingTests/InAppBillingTests.csproj delete mode 100644 src/InAppBillingTests/InAppBillingTests/MainPage.xaml delete mode 100644 src/InAppBillingTests/InAppBillingTests/MainPage.xaml.cs diff --git a/.gitignore b/.gitignore index 6b815a8..c0b20d2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ *.userprefs # Build results +.idea/ [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ diff --git a/src/.idea/.idea.InAppBilling/.idea/.name b/src/.idea/.idea.InAppBilling/.idea/.name new file mode 100644 index 0000000..8f3bed0 --- /dev/null +++ b/src/.idea/.idea.InAppBilling/.idea/.name @@ -0,0 +1 @@ +InAppBilling \ No newline at end of file diff --git a/src/.idea/.idea.InAppBilling/.idea/indexLayout.xml b/src/.idea/.idea.InAppBilling/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/src/.idea/.idea.InAppBilling/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/.idea/.idea.InAppBilling/.idea/projectSettingsUpdater.xml b/src/.idea/.idea.InAppBilling/.idea/projectSettingsUpdater.xml new file mode 100644 index 0000000..4bb9f4d --- /dev/null +++ b/src/.idea/.idea.InAppBilling/.idea/projectSettingsUpdater.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/src/.idea/.idea.InAppBilling/.idea/vcs.xml b/src/.idea/.idea.InAppBilling/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/src/.idea/.idea.InAppBilling/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/InAppBilling.sln b/src/InAppBilling.sln index a401433..340fe9c 100644 --- a/src/InAppBilling.sln +++ b/src/InAppBilling.sln @@ -29,464 +29,29 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{6A41C44D-4 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{5124C265-C6EF-4415-9497-0EF227E43095}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InAppBillingTests", "InAppBillingTests\InAppBillingTests\InAppBillingTests.csproj", "{2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InAppBillingTests.Android", "InAppBillingTests\InAppBillingTests.Android\InAppBillingTests.Android.csproj", "{4C7DF981-3F54-40EB-82AD-7D54C443FCCC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InAppBillingTests.iOS", "InAppBillingTests\InAppBillingTests.iOS\InAppBillingTests.iOS.csproj", "{096CE273-B696-42E5-8770-E06FBE982235}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InAppBillingTests.Mac", "InAppBillingTests\InAppBillingTests.Mac\InAppBillingTests.Mac.csproj", "{04BF1C8C-EACA-466D-80D8-C2B9012A37F1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InAppBillingTests.UWP", "InAppBillingTests\InAppBillingTests.UWP\InAppBillingTests.UWP.csproj", "{EBD6A824-A56E-4F33-B352-B4E72D711B00}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InAppBillingMauiTest", "InAppBillingTests\InAppBillingMauiTest\InAppBillingMauiTest.csproj", "{BAE4393A-4E17-4E60-BF53-E916505F44E1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Ad-Hoc|Any CPU = Ad-Hoc|Any CPU - Ad-Hoc|ARM = Ad-Hoc|ARM - Ad-Hoc|iPhone = Ad-Hoc|iPhone - Ad-Hoc|iPhoneSimulator = Ad-Hoc|iPhoneSimulator - Ad-Hoc|x64 = Ad-Hoc|x64 - Ad-Hoc|x86 = Ad-Hoc|x86 - AppStore|Any CPU = AppStore|Any CPU - AppStore|ARM = AppStore|ARM - AppStore|iPhone = AppStore|iPhone - AppStore|iPhoneSimulator = AppStore|iPhoneSimulator - AppStore|x64 = AppStore|x64 - AppStore|x86 = AppStore|x86 Debug|Any CPU = Debug|Any CPU - Debug|ARM = Debug|ARM - Debug|iPhone = Debug|iPhone - Debug|iPhoneSimulator = Debug|iPhoneSimulator - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU - Release|ARM = Release|ARM - Release|iPhone = Release|iPhone - Release|iPhoneSimulator = Release|iPhoneSimulator - Release|x64 = Release|x64 - Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C570E25E-259F-4D4C-88F0-B2982815192D}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Ad-Hoc|ARM.ActiveCfg = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Ad-Hoc|ARM.Build.0 = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Ad-Hoc|x64.ActiveCfg = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Ad-Hoc|x64.Build.0 = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Ad-Hoc|x86.Build.0 = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.AppStore|Any CPU.Build.0 = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.AppStore|ARM.ActiveCfg = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.AppStore|ARM.Build.0 = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.AppStore|iPhone.Build.0 = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.AppStore|x64.ActiveCfg = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.AppStore|x64.Build.0 = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.AppStore|x86.ActiveCfg = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.AppStore|x86.Build.0 = Release|Any CPU {C570E25E-259F-4D4C-88F0-B2982815192D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C570E25E-259F-4D4C-88F0-B2982815192D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Debug|ARM.ActiveCfg = Debug|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Debug|ARM.Build.0 = Debug|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Debug|iPhone.Build.0 = Debug|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Debug|x64.ActiveCfg = Debug|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Debug|x64.Build.0 = Debug|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Debug|x86.ActiveCfg = Debug|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Debug|x86.Build.0 = Debug|Any CPU {C570E25E-259F-4D4C-88F0-B2982815192D}.Release|Any CPU.ActiveCfg = Release|Any CPU {C570E25E-259F-4D4C-88F0-B2982815192D}.Release|Any CPU.Build.0 = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Release|ARM.ActiveCfg = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Release|ARM.Build.0 = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Release|iPhone.ActiveCfg = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Release|iPhone.Build.0 = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Release|x64.ActiveCfg = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Release|x64.Build.0 = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Release|x86.ActiveCfg = Release|Any CPU - {C570E25E-259F-4D4C-88F0-B2982815192D}.Release|x86.Build.0 = Release|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Ad-Hoc|x64.Build.0 = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Ad-Hoc|x86.Build.0 = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.AppStore|ARM.ActiveCfg = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.AppStore|ARM.Build.0 = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.AppStore|iPhone.Build.0 = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.AppStore|x64.ActiveCfg = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.AppStore|x64.Build.0 = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.AppStore|x86.ActiveCfg = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.AppStore|x86.Build.0 = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Debug|ARM.ActiveCfg = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Debug|ARM.Build.0 = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Debug|iPhone.Build.0 = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Debug|x64.ActiveCfg = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Debug|x64.Build.0 = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Debug|x86.ActiveCfg = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Debug|x86.Build.0 = Debug|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Release|Any CPU.Build.0 = Release|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Release|ARM.ActiveCfg = Release|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Release|ARM.Build.0 = Release|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Release|iPhone.ActiveCfg = Release|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Release|iPhone.Build.0 = Release|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Release|x64.ActiveCfg = Release|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Release|x64.Build.0 = Release|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Release|x86.ActiveCfg = Release|Any CPU - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6}.Release|x86.Build.0 = Release|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Ad-Hoc|Any CPU.Deploy.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Ad-Hoc|ARM.Deploy.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Ad-Hoc|iPhone.Deploy.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Ad-Hoc|iPhoneSimulator.Deploy.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Ad-Hoc|x64.Build.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Ad-Hoc|x64.Deploy.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Ad-Hoc|x86.Build.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Ad-Hoc|x86.Deploy.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.AppStore|Any CPU.Deploy.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.AppStore|ARM.ActiveCfg = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.AppStore|ARM.Build.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.AppStore|ARM.Deploy.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.AppStore|iPhone.Build.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.AppStore|iPhone.Deploy.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.AppStore|iPhoneSimulator.Deploy.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.AppStore|x64.ActiveCfg = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.AppStore|x64.Build.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.AppStore|x64.Deploy.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.AppStore|x86.ActiveCfg = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.AppStore|x86.Build.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.AppStore|x86.Deploy.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Debug|ARM.ActiveCfg = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Debug|ARM.Build.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Debug|ARM.Deploy.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Debug|iPhone.Build.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Debug|iPhone.Deploy.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Debug|x64.ActiveCfg = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Debug|x64.Build.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Debug|x64.Deploy.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Debug|x86.ActiveCfg = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Debug|x86.Build.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Debug|x86.Deploy.0 = Debug|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Release|Any CPU.Build.0 = Release|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Release|Any CPU.Deploy.0 = Release|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Release|ARM.ActiveCfg = Release|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Release|ARM.Build.0 = Release|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Release|ARM.Deploy.0 = Release|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Release|iPhone.ActiveCfg = Release|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Release|iPhone.Build.0 = Release|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Release|iPhone.Deploy.0 = Release|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Release|x64.ActiveCfg = Release|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Release|x64.Build.0 = Release|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Release|x64.Deploy.0 = Release|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Release|x86.ActiveCfg = Release|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Release|x86.Build.0 = Release|Any CPU - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC}.Release|x86.Deploy.0 = Release|Any CPU - {096CE273-B696-42E5-8770-E06FBE982235}.Ad-Hoc|Any CPU.ActiveCfg = Ad-Hoc|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Ad-Hoc|Any CPU.Build.0 = Ad-Hoc|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Ad-Hoc|ARM.ActiveCfg = Ad-Hoc|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Ad-Hoc|ARM.Build.0 = Ad-Hoc|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Ad-Hoc|iPhone.ActiveCfg = Ad-Hoc|iPhone - {096CE273-B696-42E5-8770-E06FBE982235}.Ad-Hoc|iPhone.Build.0 = Ad-Hoc|iPhone - {096CE273-B696-42E5-8770-E06FBE982235}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Ad-Hoc|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Ad-Hoc|iPhoneSimulator.Build.0 = Ad-Hoc|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Ad-Hoc|x64.ActiveCfg = Ad-Hoc|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Ad-Hoc|x64.Build.0 = Ad-Hoc|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Ad-Hoc|x86.ActiveCfg = Ad-Hoc|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Ad-Hoc|x86.Build.0 = Ad-Hoc|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.AppStore|Any CPU.ActiveCfg = AppStore|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.AppStore|Any CPU.Build.0 = AppStore|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.AppStore|ARM.ActiveCfg = AppStore|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.AppStore|ARM.Build.0 = AppStore|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.AppStore|iPhone.ActiveCfg = AppStore|iPhone - {096CE273-B696-42E5-8770-E06FBE982235}.AppStore|iPhone.Build.0 = AppStore|iPhone - {096CE273-B696-42E5-8770-E06FBE982235}.AppStore|iPhoneSimulator.ActiveCfg = AppStore|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.AppStore|iPhoneSimulator.Build.0 = AppStore|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.AppStore|x64.ActiveCfg = AppStore|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.AppStore|x64.Build.0 = AppStore|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.AppStore|x86.ActiveCfg = AppStore|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.AppStore|x86.Build.0 = AppStore|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Debug|Any CPU.Build.0 = Debug|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Debug|ARM.ActiveCfg = Debug|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Debug|ARM.Build.0 = Debug|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Debug|iPhone.ActiveCfg = Debug|iPhone - {096CE273-B696-42E5-8770-E06FBE982235}.Debug|iPhone.Build.0 = Debug|iPhone - {096CE273-B696-42E5-8770-E06FBE982235}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Debug|x64.ActiveCfg = Debug|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Debug|x64.Build.0 = Debug|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Debug|x86.ActiveCfg = Debug|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Debug|x86.Build.0 = Debug|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Release|Any CPU.ActiveCfg = Release|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Release|Any CPU.Build.0 = Release|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Release|ARM.ActiveCfg = Release|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Release|ARM.Build.0 = Release|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Release|iPhone.ActiveCfg = Release|iPhone - {096CE273-B696-42E5-8770-E06FBE982235}.Release|iPhone.Build.0 = Release|iPhone - {096CE273-B696-42E5-8770-E06FBE982235}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Release|x64.ActiveCfg = Release|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Release|x64.Build.0 = Release|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Release|x86.ActiveCfg = Release|iPhoneSimulator - {096CE273-B696-42E5-8770-E06FBE982235}.Release|x86.Build.0 = Release|iPhoneSimulator - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Ad-Hoc|x64.Build.0 = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Ad-Hoc|x86.Build.0 = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.AppStore|ARM.ActiveCfg = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.AppStore|ARM.Build.0 = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.AppStore|iPhone.Build.0 = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.AppStore|x64.ActiveCfg = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.AppStore|x64.Build.0 = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.AppStore|x86.ActiveCfg = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.AppStore|x86.Build.0 = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Debug|ARM.ActiveCfg = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Debug|ARM.Build.0 = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Debug|iPhone.Build.0 = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Debug|x64.ActiveCfg = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Debug|x64.Build.0 = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Debug|x86.ActiveCfg = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Debug|x86.Build.0 = Debug|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Release|Any CPU.Build.0 = Release|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Release|ARM.ActiveCfg = Release|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Release|ARM.Build.0 = Release|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Release|iPhone.ActiveCfg = Release|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Release|iPhone.Build.0 = Release|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Release|x64.ActiveCfg = Release|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Release|x64.Build.0 = Release|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Release|x86.ActiveCfg = Release|Any CPU - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1}.Release|x86.Build.0 = Release|Any CPU - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Ad-Hoc|Any CPU.ActiveCfg = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Ad-Hoc|Any CPU.Build.0 = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Ad-Hoc|Any CPU.Deploy.0 = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Ad-Hoc|ARM.ActiveCfg = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Ad-Hoc|ARM.Build.0 = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Ad-Hoc|ARM.Deploy.0 = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Ad-Hoc|iPhone.ActiveCfg = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Ad-Hoc|iPhone.Build.0 = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Ad-Hoc|iPhone.Deploy.0 = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Ad-Hoc|iPhoneSimulator.Deploy.0 = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Ad-Hoc|x64.ActiveCfg = Debug|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Ad-Hoc|x64.Build.0 = Debug|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Ad-Hoc|x64.Deploy.0 = Debug|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Ad-Hoc|x86.ActiveCfg = Debug|x86 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Ad-Hoc|x86.Build.0 = Debug|x86 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Ad-Hoc|x86.Deploy.0 = Debug|x86 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.AppStore|Any CPU.ActiveCfg = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.AppStore|Any CPU.Build.0 = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.AppStore|Any CPU.Deploy.0 = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.AppStore|ARM.ActiveCfg = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.AppStore|ARM.Build.0 = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.AppStore|ARM.Deploy.0 = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.AppStore|iPhone.ActiveCfg = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.AppStore|iPhone.Build.0 = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.AppStore|iPhone.Deploy.0 = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.AppStore|iPhoneSimulator.ActiveCfg = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.AppStore|iPhoneSimulator.Build.0 = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.AppStore|iPhoneSimulator.Deploy.0 = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.AppStore|x64.ActiveCfg = Debug|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.AppStore|x64.Build.0 = Debug|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.AppStore|x64.Deploy.0 = Debug|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.AppStore|x86.ActiveCfg = Debug|x86 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.AppStore|x86.Build.0 = Debug|x86 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.AppStore|x86.Deploy.0 = Debug|x86 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Debug|Any CPU.ActiveCfg = Debug|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Debug|Any CPU.Build.0 = Debug|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Debug|Any CPU.Deploy.0 = Debug|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Debug|ARM.ActiveCfg = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Debug|ARM.Build.0 = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Debug|ARM.Deploy.0 = Debug|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Debug|iPhone.ActiveCfg = Debug|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Debug|iPhone.Build.0 = Debug|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Debug|iPhone.Deploy.0 = Debug|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Debug|iPhoneSimulator.ActiveCfg = Debug|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Debug|iPhoneSimulator.Build.0 = Debug|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Debug|iPhoneSimulator.Deploy.0 = Debug|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Debug|x64.ActiveCfg = Debug|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Debug|x64.Build.0 = Debug|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Debug|x64.Deploy.0 = Debug|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Debug|x86.ActiveCfg = Debug|x86 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Debug|x86.Build.0 = Debug|x86 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Debug|x86.Deploy.0 = Debug|x86 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Release|Any CPU.ActiveCfg = Release|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Release|Any CPU.Build.0 = Release|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Release|Any CPU.Deploy.0 = Release|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Release|ARM.ActiveCfg = Release|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Release|ARM.Build.0 = Release|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Release|ARM.Deploy.0 = Release|ARM - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Release|iPhone.ActiveCfg = Release|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Release|iPhone.Build.0 = Release|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Release|iPhone.Deploy.0 = Release|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Release|iPhoneSimulator.ActiveCfg = Release|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Release|iPhoneSimulator.Build.0 = Release|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Release|iPhoneSimulator.Deploy.0 = Release|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Release|x64.ActiveCfg = Release|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Release|x64.Build.0 = Release|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Release|x64.Deploy.0 = Release|x64 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Release|x86.ActiveCfg = Release|x86 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Release|x86.Build.0 = Release|x86 - {EBD6A824-A56E-4F33-B352-B4E72D711B00}.Release|x86.Deploy.0 = Release|x86 - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Ad-Hoc|Any CPU.Deploy.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Ad-Hoc|ARM.Deploy.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Ad-Hoc|iPhone.Deploy.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Ad-Hoc|iPhoneSimulator.Deploy.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Ad-Hoc|x64.Build.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Ad-Hoc|x64.Deploy.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Ad-Hoc|x86.Build.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Ad-Hoc|x86.Deploy.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.AppStore|Any CPU.Deploy.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.AppStore|ARM.ActiveCfg = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.AppStore|ARM.Build.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.AppStore|ARM.Deploy.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.AppStore|iPhone.Build.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.AppStore|iPhone.Deploy.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.AppStore|iPhoneSimulator.Deploy.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.AppStore|x64.ActiveCfg = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.AppStore|x64.Build.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.AppStore|x64.Deploy.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.AppStore|x86.ActiveCfg = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.AppStore|x86.Build.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.AppStore|x86.Deploy.0 = Debug|Any CPU {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Debug|Any CPU.Build.0 = Debug|Any CPU {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Debug|ARM.ActiveCfg = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Debug|ARM.Build.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Debug|ARM.Deploy.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Debug|iPhone.Build.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Debug|iPhone.Deploy.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Debug|x64.ActiveCfg = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Debug|x64.Build.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Debug|x64.Deploy.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Debug|x86.ActiveCfg = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Debug|x86.Build.0 = Debug|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Debug|x86.Deploy.0 = Debug|Any CPU {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Release|Any CPU.ActiveCfg = Release|Any CPU {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Release|Any CPU.Build.0 = Release|Any CPU {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Release|Any CPU.Deploy.0 = Release|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Release|ARM.ActiveCfg = Release|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Release|ARM.Build.0 = Release|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Release|ARM.Deploy.0 = Release|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Release|iPhone.ActiveCfg = Release|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Release|iPhone.Build.0 = Release|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Release|iPhone.Deploy.0 = Release|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Release|x64.ActiveCfg = Release|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Release|x64.Build.0 = Release|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Release|x64.Deploy.0 = Release|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Release|x86.ActiveCfg = Release|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Release|x86.Build.0 = Release|Any CPU - {BAE4393A-4E17-4E60-BF53-E916505F44E1}.Release|x86.Deploy.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {2E99E9AA-B6D4-4F7F-8D8B-F922D854F7C6} = {5124C265-C6EF-4415-9497-0EF227E43095} - {4C7DF981-3F54-40EB-82AD-7D54C443FCCC} = {5124C265-C6EF-4415-9497-0EF227E43095} - {096CE273-B696-42E5-8770-E06FBE982235} = {5124C265-C6EF-4415-9497-0EF227E43095} - {04BF1C8C-EACA-466D-80D8-C2B9012A37F1} = {5124C265-C6EF-4415-9497-0EF227E43095} - {EBD6A824-A56E-4F33-B352-B4E72D711B00} = {5124C265-C6EF-4415-9497-0EF227E43095} {BAE4393A-4E17-4E60-BF53-E916505F44E1} = {5124C265-C6EF-4415-9497-0EF227E43095} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/InAppBillingTests/InAppBillingMauiTest/AppShell.xaml b/src/InAppBillingTests/InAppBillingMauiTest/AppShell.xaml index eafe6a5..f6bf581 100644 --- a/src/InAppBillingTests/InAppBillingMauiTest/AppShell.xaml +++ b/src/InAppBillingTests/InAppBillingMauiTest/AppShell.xaml @@ -3,7 +3,7 @@ x:Class="InAppBillingMauiTest.AppShell" xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" - xmlns:local="clr-namespace:InAppBillingMauiTest" + xmlns:local="clr-namespace:InAppBillingTests" Shell.FlyoutBehavior="Disabled"> - - net6.0-android;net6.0-ios;net6.0-maccatalyst - $(TargetFrameworks);net6.0-windows10.0.19041 - Exe - InAppBillingMauiTest - true - true - enable - - - InAppBillingMauiTest - - - com.companyname.inappbillingmauitest - 8C7E9190-68F2-4D7E-BF0E-C1E8A260B4D5 - - - 1.0 - 1 - - 14.2 - 14.0 - 21.0 - 10.0.17763.0 - 10.0.17763.0 - - - - - - - - - - - - - - - - - - - - - - + + net8.0-android;net8.0-ios;net8.0-maccatalyst + $(TargetFrameworks);net8.0-windows10.0.19041 + Exe + InAppBillingMauiTest + true + true + enable + + + InAppBillingMauiTest + + + com.companyname.inappbillingmauitest + 8C7E9190-68F2-4D7E-BF0E-C1E8A260B4D5 + + + 1.0 + 1 + + 14.2 + 14.0 + 21.0 + 10.0.17763.0 + 10.0.17763.0 + + + + + + + + + + + + + + + + + + + + + + - - - - - diff --git a/src/InAppBillingTests/InAppBillingMauiTest/MainPage.xaml b/src/InAppBillingTests/InAppBillingMauiTest/MainPage.xaml index 40ae8a9..92d5a3e 100644 --- a/src/InAppBillingTests/InAppBillingMauiTest/MainPage.xaml +++ b/src/InAppBillingTests/InAppBillingMauiTest/MainPage.xaml @@ -1,45 +1,23 @@  + xmlns:iap="clr-namespace:Plugin.InAppBilling;assembly=Plugin.InAppBilling" + x:Class="InAppBillingTests.MainPage"> - - - - public class CrossInAppBilling { - static Lazy implementation = new Lazy(() => CreateInAppBilling(), System.Threading.LazyThreadSafetyMode.PublicationOnly); + static Lazy implementation = new(() => CreateInAppBilling(), System.Threading.LazyThreadSafetyMode.PublicationOnly); /// @@ -31,16 +31,7 @@ public static IInAppBilling Current } } - static IInAppBilling CreateInAppBilling() - { -#if NETSTANDARD1_0 || NETSTANDARD2_0 || NETSTANDARD - return null; -#else -#pragma warning disable IDE0022 // Use expression body for methods - return new InAppBillingImplementation(); -#pragma warning restore IDE0022 // Use expression body for methods -#endif - } + static IInAppBilling CreateInAppBilling() => new InAppBillingImplementation(); internal static Exception NotImplementedInReferenceAssembly() => new NotImplementedException("This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation."); diff --git a/src/Plugin.InAppBilling/InAppBilling.android.cs b/src/Plugin.InAppBilling/InAppBilling.android.cs index e11a5ef..76fa9c3 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -71,7 +71,7 @@ void AssertPurchaseTransactionReady() /// Connect to billing service /// /// If Success - public override Task ConnectAsync(bool enablePendingPurchases = true, CancellationToken cancellationToken) + public override Task ConnectAsync(bool enablePendingPurchases = true, CancellationToken cancellationToken = default) { tcsPurchase?.TrySetCanceled(); tcsPurchase = null; @@ -237,7 +237,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, CancellationToken cancellationToken) + public override async Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration, CancellationToken cancellationToken = default) { // If we have a current task and it is not completed then return null. diff --git a/src/Plugin.InAppBilling/InAppBilling.apple.cs b/src/Plugin.InAppBilling/InAppBilling.apple.cs index 3ac7f29..98b1201 100644 --- a/src/Plugin.InAppBilling/InAppBilling.apple.cs +++ b/src/Plugin.InAppBilling/InAppBilling.apple.cs @@ -320,7 +320,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, string subOfferToken = null, CancellationToken cancellationToken) + public async override Task PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null, string subOfferToken = null, CancellationToken cancellationToken = default) { Init(); var p = await PurchaseAsync(productId, itemType, obfuscatedAccountId, cancellationToken); @@ -440,7 +440,7 @@ async Task PurchaseAsync(string productId, ItemType itemTy /// (iOS not supported) Apple store manages upgrades natively when subscriptions of the same group are purchased. /// /// iOS not supported - public override Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration, CancellationToken cancellationToken) => + public override Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration, CancellationToken cancellationToken = default) => throw new NotImplementedException("iOS not supported. Apple store manages upgrades natively when subscriptions of the same group are purchased."); diff --git a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj index bd108ae..84ac5d6 100644 --- a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj +++ b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj @@ -1,15 +1,14 @@ - + - - netstandard2.0;MonoAndroid13.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; - 10.0 + net8.0-android;net8.0-ios;net8.0-maccatalyst;net8.0-tvos;net8.0-macos + $(TargetFrameworks);net8.0-windows10.0.19041; + latest Plugin.InAppBilling Plugin.InAppBilling $(AssemblyName) ($(TargetFramework)) - 6.0.0.0 - 6.0.0.0 - 6.0.0.0 + 7.0.0.0 + 7.0.0.0 + 7.0.0.0 James Montemagno Plugin.InAppBilling true @@ -25,9 +24,9 @@ .NET MAUI, Xamarin, and Windows Plugin to In-App Billing. Get item information, purchase items, and restore purchases with a cross-platform API. Read the full documenation on the projects page. - Copyright 2022 + Copyright 2024 https://github.com/jamesmontemagno/InAppBillingPlugin - See: https://github.com/jamesmontemagno/InAppBillingPlugin + See: https://github.com/jamesmontemagno/InAppBillingPlugin en false @@ -36,18 +35,11 @@ 10.0 - 10.0 - 13.1 - 10.14 21.0 10.0.17763.0 10.0.17763.0 - - 10.0.17763.0 - - full true @@ -62,89 +54,40 @@ true $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + - + - - - - - - - - - - - - - + + - + - - - - - true - - + - - - + - - - - - - - - - - + + + + - + - - - - - - - - - - - - - + - + - + - - - - C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\Extensions\Xamarin.VisualStudio\Xamarin.Mac.dll - C:\Program Files\Microsoft Visual Studio\2022\Professional\Common7\IDE\Extensions\Xamarin.VisualStudio\Xamarin.Mac.dll - C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\Extensions\Xamarin.VisualStudio\Xamarin.Mac.dll - C:\Program Files\Microsoft Visual Studio\2022\Preview\Common7\IDE\Extensions\Xamarin.VisualStudio\Xamarin.Mac.dll - - - + diff --git a/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs b/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs index cc72d5f..1c0207b 100644 --- a/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs +++ b/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs @@ -157,7 +157,7 @@ public virtual void Dispose(bool disposing) /// /// /// - public virtual Task> FinalizePurchaseOfProductAsync(params string[] productIds, CancellationToken cancellationToken = default) => Task.FromResult(new List<(string Id, bool Success)>().AsEnumerable()); + public virtual Task> FinalizePurchaseOfProductAsync(string[] productIds, CancellationToken cancellationToken = default) => Task.FromResult(new List<(string Id, bool Success)>().AsEnumerable()); /// /// iOS: Displays a sheet that enables users to redeem subscription offer codes that you configure in App Store Connect. From c27cebc5fe15a8b06ccdc08c8bce29c8e7268d33 Mon Sep 17 00:00:00 2001 From: Allan Ritchie Date: Thu, 23 May 2024 21:55:24 -0400 Subject: [PATCH 06/22] Update docs (I think - these looked out of date) --- docs/CheckAndRestorePurchases.md | 3 ++- docs/PurchaseConsumable.md | 6 ++++-- docs/PurchaseNonConsumable.md | 5 +++-- docs/PurchaseSubscription.md | 5 +++-- docs/SecuringPurchases.md | 2 +- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/CheckAndRestorePurchases.md b/docs/CheckAndRestorePurchases.md index ad20ec3..c1217fb 100644 --- a/docs/CheckAndRestorePurchases.md +++ b/docs/CheckAndRestorePurchases.md @@ -6,8 +6,9 @@ When users get a new device or re-install your application it is best practice t /// Get all current purchases for a specified product type. /// /// Type of product +/// Cancel the request /// The current purchases -Task> GetPurchasesAsync(ItemType itemType); +Task> GetPurchasesAsync(ItemType itemType, CancellationToken cancellationToken = default); ``` When you make a call to restore a purchase it will prompt for the user to sign in if they haven't yet, so take that into consideration. diff --git a/docs/PurchaseConsumable.md b/docs/PurchaseConsumable.md index 7bbf337..69fc96b 100644 --- a/docs/PurchaseConsumable.md +++ b/docs/PurchaseConsumable.md @@ -23,9 +23,10 @@ Consumables are unique and work a bit different on each platform and the `Consum /// Type of product being requested /// 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. +/// Cancel the request. /// 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, CancellationToken cancellationToken = default); ``` #### obfuscatedAccountId & obfuscatedProfileId @@ -44,9 +45,10 @@ Task PurchaseAsync(string productId, ItemType itemType, st /// /// Id or Sku of product /// Original Purchase Token +/// Cancel the request /// If consumed successful /// If an error occurs during processing -Task ConsumePurchaseAsync(string productId, string transactionIdentifier); +Task ConsumePurchaseAsync(string productId, string transactionIdentifier, CancellationToken cancellationToken = default); ``` diff --git a/docs/PurchaseNonConsumable.md b/docs/PurchaseNonConsumable.md index e190880..56086a7 100644 --- a/docs/PurchaseNonConsumable.md +++ b/docs/PurchaseNonConsumable.md @@ -17,9 +17,10 @@ All purchases go through the `PurchaseAsync` method and you must always `Connect /// Type of product being requested /// 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. +/// Cancel the request /// 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, CancellationToken cancellationToken = default); ``` On Android you must call `FinalizePurchaseAsync` within 3 days when a purchase is validated. Please read the [Android documentation on Pending Transactions](https://developer.android.com/google/play/billing/integrate#pending) for more information. @@ -52,7 +53,7 @@ public async Task PurchaseItem(string productId) else if(purchase.State == PurchaseState.Purchased) { // only need to finalize if on Android unless you turn off auto finalize on iOS - var ack = await CrossInAppBilling.Current.FinalizePurchaseAsync(purchase.TransactionIdentifier); + var ack = await CrossInAppBilling.Current.FinalizePurchaseAsync([purchase.TransactionIdentifier]); // Handle if acknowledge was successful or not } diff --git a/docs/PurchaseSubscription.md b/docs/PurchaseSubscription.md index d613685..443e569 100644 --- a/docs/PurchaseSubscription.md +++ b/docs/PurchaseSubscription.md @@ -17,9 +17,10 @@ All purchases go through the `PurchaseAsync` method and you must always `Connect /// Type of product being requested /// 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. +/// Cancel the request. /// 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, CancellationToken cancellationToken = default); ``` On Android you must call `FinalizePurchaseAsync` within 3 days when a purchase is validated. Please read the [Android documentation on Pending Transactions](https://developer.android.com/google/play/billing/integrate#pending) for more information. @@ -52,7 +53,7 @@ public async Task PurchaseItem(string productId, string payload) else if(purchase.State == PurchaseState.Purchased) { //only needed on android unless you turn off auto finalize - var ack = await CrossInAppBilling.Current.FinalizePurchaseAsync(purchase.TransactionIdentifier); + var ack = await CrossInAppBilling.Current.FinalizePurchaseAsync([purchase.TransactionIdentifier]); // Handle if acknowledge was successful or not } diff --git a/docs/SecuringPurchases.md b/docs/SecuringPurchases.md index 2a67d93..c739d9e 100644 --- a/docs/SecuringPurchases.md +++ b/docs/SecuringPurchases.md @@ -3,7 +3,7 @@ Each platform handles security of In-App Purchases a bit different. To handle this whenever you make a purchase you should use the date from the purchase to validate on your backend. ## Recommended Reading: -* [Xamarin.iOS Securing Purchases Documentation](https://developer.xamarin.com/guides/ios/platform_features/in-app_purchasing/transactions_and_verification/#Securing_Purchases) +* [iOS Securing Purchases Documentation](https://developer.xamarin.com/guides/ios/platform_features/in-app_purchasing/transactions_and_verification/#Securing_Purchases) * [Google Play service Security and Design](https://developer.android.com/google/play/billing/billing_best_practices.html) From dd123b09ac27259e9991dbff1b3ff60a9c19cb95 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Fri, 24 May 2024 21:16:28 -0700 Subject: [PATCH 07/22] bump --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8136e6d..49d18be 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ -## In-App Billing Plugin for .NET MAUI, Xamarin, and Windows +## In-App Billing Plugin for .NET MAUI and Windows -A simple In-App Purchase plugin for .NET MAUI, Xamarin, and Windows to query item information, purchase items, restore items, and more. +A simple In-App Purchase plugin for .NET MAUI and Windows to query item information, purchase items, restore items, and more. Subscriptions are supported on iOS, Android, and Mac. Windows/UWP/WinUI 3 - does not support subscriptions at this time. ## Important Version Information +* v8 now supports .NET 8+ .NET MAUI and Windows Apps. * v7 now supports .NET 6+, .NET MAUI, UWP, and Xamarin/Xamarin.Forms projects * v7 is built against Android Billing Library 6.0 * See migration guides below From b12a494984b591a2208a88bebe4747ad2692a492 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Fri, 24 May 2024 21:21:00 -0700 Subject: [PATCH 08/22] delete .idea folder --- src/.idea/.idea.InAppBilling/.idea/.name | 1 - src/.idea/.idea.InAppBilling/.idea/indexLayout.xml | 8 -------- .../.idea.InAppBilling/.idea/projectSettingsUpdater.xml | 6 ------ src/.idea/.idea.InAppBilling/.idea/vcs.xml | 6 ------ 4 files changed, 21 deletions(-) delete mode 100644 src/.idea/.idea.InAppBilling/.idea/.name delete mode 100644 src/.idea/.idea.InAppBilling/.idea/indexLayout.xml delete mode 100644 src/.idea/.idea.InAppBilling/.idea/projectSettingsUpdater.xml delete mode 100644 src/.idea/.idea.InAppBilling/.idea/vcs.xml diff --git a/src/.idea/.idea.InAppBilling/.idea/.name b/src/.idea/.idea.InAppBilling/.idea/.name deleted file mode 100644 index 8f3bed0..0000000 --- a/src/.idea/.idea.InAppBilling/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -InAppBilling \ No newline at end of file diff --git a/src/.idea/.idea.InAppBilling/.idea/indexLayout.xml b/src/.idea/.idea.InAppBilling/.idea/indexLayout.xml deleted file mode 100644 index 7b08163..0000000 --- a/src/.idea/.idea.InAppBilling/.idea/indexLayout.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/.idea/.idea.InAppBilling/.idea/projectSettingsUpdater.xml b/src/.idea/.idea.InAppBilling/.idea/projectSettingsUpdater.xml deleted file mode 100644 index 4bb9f4d..0000000 --- a/src/.idea/.idea.InAppBilling/.idea/projectSettingsUpdater.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/.idea/.idea.InAppBilling/.idea/vcs.xml b/src/.idea/.idea.InAppBilling/.idea/vcs.xml deleted file mode 100644 index 6c0b863..0000000 --- a/src/.idea/.idea.InAppBilling/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 8568f0d4f0b7f004df3296678cca43df548555b1 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Fri, 24 May 2024 21:22:19 -0700 Subject: [PATCH 09/22] add in --- .gitignore | 1 + README.md | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index c0b20d2..0b3c7ce 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ # Build results .idea/ +src/.idea/ [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ diff --git a/README.md b/README.md index 49d18be..7b7733b 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,11 @@ Get started by reading through the [In-App Billing Plugin documentation](https:/ |Platform|Version| | ------------------- | :------------------: | -|Xamarin.iOS & iOS for .NET|10+| -|Xamarin.Mac, macOS for .NET, macCatlyst for .NET |All| -|Xamarin.TVOS, tvOS for .NET|10.13.2| -|Xamarin.Android, Android for .NET|21+| -|Windows 10 UWP|10+| +|iOS for .NET|10+| +|macCatlyst for .NET |All| +|tvOS for .NET|10.13.2| +|Android for .NET|21+| |Windows App SDK (WinUI 3) |10+| -|Xamarin.Forms|All| |.NET MAUI|All| ### Created By: [@JamesMontemagno](http://github.com/jamesmontemagno) From ad132afa5063b9c71ea31279c34d46be3c159872 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Fri, 24 May 2024 21:38:29 -0700 Subject: [PATCH 10/22] updates --- src/Plugin.InAppBilling/Plugin.InAppBilling.csproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj index f478ac0..f8c6431 100644 --- a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj +++ b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj @@ -17,11 +17,11 @@ https://github.com/jamesmontemagno/InAppBillingPlugin/blob/master/LICENSE JamesMontemagno https://github.com/jamesmontemagno/InAppBillingPlugin - Xamarin, .NET MAUI, and Windows plugin to In-App Billing. + .NET MAUI, and Windows plugin to In-App Billing. .net maui, macos, windows, xamarin, xamarin.forms, android, ios, uwp, windows phone, In-App Billing, purchases, plugin - In-App Billing Plugin for .NET MAUI, Xamarin, and Windows + In-App Billing Plugin for .NET MAUI and Windows - .NET MAUI, Xamarin, and Windows Plugin to In-App Billing. Get item information, purchase items, and restore purchases with a cross-platform API. + .NET MAUI and Windows Plugin to In-App Billing. Get item information, purchase items, and restore purchases with a cross-platform API. Read the full documenation on the projects page. Copyright 2024 @@ -65,14 +65,14 @@ - + - - + + From ead67a820a2d4ee46f5bc4dc0b7d2491c36381d3 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Fri, 24 May 2024 21:51:45 -0700 Subject: [PATCH 11/22] windows updates --- src/Plugin.InAppBilling/InAppBilling.uwp.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Plugin.InAppBilling/InAppBilling.uwp.cs b/src/Plugin.InAppBilling/InAppBilling.uwp.cs index 5cca442..68ec528 100644 --- a/src/Plugin.InAppBilling/InAppBilling.uwp.cs +++ b/src/Plugin.InAppBilling/InAppBilling.uwp.cs @@ -33,7 +33,7 @@ public InAppBillingImplementation() /// Type of item /// Product Ids /// - public async override Task> GetProductInfoAsync(ItemType itemType, string[] productIds, CancellationToken cancellationToken) + public async override Task> GetProductInfoAsync(ItemType itemType, string[] productIds, CancellationToken cancellationToken = default) { // Get list of products from store or simulator var listingInformation = await CurrentAppMock.LoadListingInformationAsync(InTestingMode); @@ -77,7 +77,7 @@ public async override Task> GetProductInfoAsync /// /// /// - public async override Task> GetPurchasesAsync(ItemType itemType, CancellationToken cancellationToken) + public async override Task> GetPurchasesAsync(ItemType itemType, CancellationToken cancellationToken = default) { // Get list of product receipts from store or simulator var xmlReceipt = await CurrentAppMock.GetAppReceiptAsync(InTestingMode); @@ -95,7 +95,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, string subOfferToken = null, CancellationToken cancellationToken) + public async override Task PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null, string subOfferToken = null, CancellationToken cancellationToken = default) { // Get purchase result from store or simulator var purchaseResult = await CurrentAppMock.RequestProductPurchaseAsync(InTestingMode, productId); @@ -116,7 +116,7 @@ public async override Task PurchaseAsync(string productId, /// (UWP not supported) Upgrade/Downgrade/Change a previously purchased subscription /// /// UWP not supported - public override Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration, CancellationToken cancellationToken) => + public override Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration, CancellationToken cancellationToken = default) => throw new NotImplementedException("UWP not supported."); /// @@ -126,7 +126,7 @@ public override Task UpgradePurchasedSubscriptionAsync(str /// Original Purchase Token /// If consumed successful /// If an error occures during processing - public async override Task ConsumePurchaseAsync(string productId, string transactionIdentifier, CancellationToken cancellationToken) + public async override Task ConsumePurchaseAsync(string productId, string transactionIdentifier, CancellationToken cancellationToken = default) { var result = await CurrentAppMock.ReportConsumableFulfillmentAsync(InTestingMode, productId, new Guid(transactionIdentifier)); return result switch From 04a5130b7c1bb80e22e71a5a4f09053b486e8048 Mon Sep 17 00:00:00 2001 From: Christian Lavallee Date: Mon, 10 Jun 2024 15:20:36 -0400 Subject: [PATCH 12/22] Add obfuscatedAccountId and obfuscatedProfileId to UpgradePurchasedSubscription --- .../InAppBilling.android.cs | 19 +++++++++++++------ src/Plugin.InAppBilling/InAppBilling.apple.cs | 2 +- src/Plugin.InAppBilling/InAppBilling.uwp.cs | 2 +- .../Shared/BaseInAppBilling.shared.cs | 2 +- .../Shared/IInAppBilling.shared.cs | 2 +- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/Plugin.InAppBilling/InAppBilling.android.cs b/src/Plugin.InAppBilling/InAppBilling.android.cs index f43fea5..dda0d80 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -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, string obfuscatedAccountId = null, string obfuscatedProfileId = null) { if (BillingClient == null || !IsConnected) { @@ -236,12 +236,12 @@ public override async Task UpgradePurchasedSubscriptionAsy return null; } - var purchase = await UpgradePurchasedSubscriptionInternalAsync(newProductId, purchaseTokenOfOriginalSubscription, prorationMode); + var purchase = await UpgradePurchasedSubscriptionInternalAsync(newProductId, purchaseTokenOfOriginalSubscription, prorationMode, obfuscatedAccountId, obfuscatedProfileId); return purchase; } - async Task UpgradePurchasedSubscriptionInternalAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode) + async Task UpgradePurchasedSubscriptionInternalAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode, string obfuscatedAccountId, string obfuscatedProfileId) { var itemType = ProductType.Subs; @@ -282,10 +282,17 @@ async Task UpgradePurchasedSubscriptionInternalAsync(strin var prodDetailsParams = string.IsNullOrWhiteSpace(t) ? prodDetails.Build() : prodDetails.SetOfferToken(t).Build(); - var flowParams = BillingFlowParams.NewBuilder() + var billingFlowParams = BillingFlowParams.NewBuilder() .SetProductDetailsParamsList(new[] { prodDetailsParams }) - .SetSubscriptionUpdateParams(updateParams) - .Build(); + .SetSubscriptionUpdateParams(updateParams); + + if (!string.IsNullOrWhiteSpace(obfuscatedAccountId)) + billingFlowParams.SetObfuscatedAccountId(obfuscatedAccountId); + + if (!string.IsNullOrWhiteSpace(obfuscatedProfileId)) + billingFlowParams.SetObfuscatedProfileId(obfuscatedProfileId); + + var flowParams = billingFlowParams.Build(); tcsPurchase = new TaskCompletionSource<(BillingResult billingResult, IList purchases)>(); var responseCode = BillingClient.LaunchBillingFlow(Activity, flowParams); diff --git a/src/Plugin.InAppBilling/InAppBilling.apple.cs b/src/Plugin.InAppBilling/InAppBilling.apple.cs index 4465904..b0e3d95 100644 --- a/src/Plugin.InAppBilling/InAppBilling.apple.cs +++ b/src/Plugin.InAppBilling/InAppBilling.apple.cs @@ -431,7 +431,7 @@ async Task PurchaseAsync(string productId, ItemType itemTy /// (iOS not supported) Apple store manages upgrades natively when subscriptions of the same group are purchased. /// /// iOS not supported - public override Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration) => + public override Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration, string obfuscatedProfileId = null, string subOfferToken = null) => throw new NotImplementedException("iOS not supported. Apple store manages upgrades natively when subscriptions of the same group are purchased."); diff --git a/src/Plugin.InAppBilling/InAppBilling.uwp.cs b/src/Plugin.InAppBilling/InAppBilling.uwp.cs index 33d0110..673b184 100644 --- a/src/Plugin.InAppBilling/InAppBilling.uwp.cs +++ b/src/Plugin.InAppBilling/InAppBilling.uwp.cs @@ -115,7 +115,7 @@ public async override Task PurchaseAsync(string productId, /// (UWP not supported) Upgrade/Downgrade/Change a previously purchased subscription /// /// UWP not supported - public override Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration) => + public override Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration, string obfuscatedAccountId = null, string obfuscatedProfileId = null) => throw new NotImplementedException("UWP not supported."); /// diff --git a/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs b/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs index 9f06d30..f2103be 100644 --- a/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs +++ b/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs @@ -99,7 +99,7 @@ public virtual Task> GetPurchasesHistoryAsync( /// Purchase token of original subscription (can not be null) /// Proration mode /// Purchase details - public abstract Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration); + public abstract Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration, string obfuscatedAccountId = null, string obfuscatedProfileId = null); /// /// Consume a purchase with a purchase token. diff --git a/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs b/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs index b4fe744..fdd54c4 100644 --- a/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs +++ b/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs @@ -101,7 +101,7 @@ public interface IInAppBilling : IDisposable /// Proration mode (1 - ImmediateWithTimeProration, 2 - ImmediateAndChargeProratedPrice, 3 - ImmediateWithoutProration, 4 - Deferred) /// Purchase details /// If an error occurs during processing - Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration); + Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration, string obfuscatedAccountId = null, string obfuscatedProfileId = null); /// /// Consume a purchase with a purchase token. From 82669153cf9c0c97549973fc057fcd0ca9de09f8 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Tue, 16 Jul 2024 15:47:05 -0700 Subject: [PATCH 13/22] update a few packages --- nuget/readme.txt | 5 ++++- src/Plugin.InAppBilling/AssemblyIncludes.android.cs | 1 - src/Plugin.InAppBilling/InAppBilling.android.cs | 2 +- src/Plugin.InAppBilling/Plugin.InAppBilling.csproj | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) delete mode 100644 src/Plugin.InAppBilling/AssemblyIncludes.android.cs diff --git a/nuget/readme.txt b/nuget/readme.txt index 5ceb5f5..6d582f8 100644 --- a/nuget/readme.txt +++ b/nuget/readme.txt @@ -1,4 +1,7 @@ -In-App Billing Plugin for .NET MAUI, Xamarin, & Windows +In-App Billing Plugin for .NET MAUI + +Version 8.0+ - .NET 8+ +1. Updated APIs and you must target .NET 8 Version 7.0+ - Major Android updates 1.) You must compile and target against Android 12 or higher diff --git a/src/Plugin.InAppBilling/AssemblyIncludes.android.cs b/src/Plugin.InAppBilling/AssemblyIncludes.android.cs deleted file mode 100644 index c1b984b..0000000 --- a/src/Plugin.InAppBilling/AssemblyIncludes.android.cs +++ /dev/null @@ -1 +0,0 @@ -//[assembly: Android.App.MetaData("com.google.android.play.billingclient.version", Value = "6.0.1")] diff --git a/src/Plugin.InAppBilling/InAppBilling.android.cs b/src/Plugin.InAppBilling/InAppBilling.android.cs index 69172a7..003728d 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -29,7 +29,7 @@ public class InAppBillingImplementation : BaseInAppBilling /// /// The context. 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."); + Platform.CurrentActivity ?? throw new NullReferenceException("Current Activity is null, ensure that the MainActivity.cs file is configuring .NET MAUI in your source code so the In App Billing can use it."); static Context Context => Application.Context; diff --git a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj index f8c6431..c33480f 100644 --- a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj +++ b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj @@ -71,8 +71,8 @@ - - + + From 37752fa0fe6bf36ad8b2ee553c9b5fd4ea31a9a6 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Tue, 16 Jul 2024 16:03:20 -0700 Subject: [PATCH 14/22] fix nuget --- src/Plugin.InAppBilling/Plugin.InAppBilling.csproj | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj index c33480f..3a46079 100644 --- a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj +++ b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj @@ -68,9 +68,12 @@ + + true + + - From 4279aac0ac5d9f1edcdbc5422972aa2d3b49e920 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Tue, 16 Jul 2024 16:12:26 -0700 Subject: [PATCH 15/22] more updated --- src/Plugin.InAppBilling/Plugin.InAppBilling.csproj | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj index 3a46079..3e682a9 100644 --- a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj +++ b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj @@ -67,13 +67,10 @@ - - - true - + From 04c84c3018418540df333bdaa9c1f21426215d76 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Tue, 16 Jul 2024 16:24:45 -0700 Subject: [PATCH 16/22] cleanup apis --- src/Plugin.InAppBilling/InAppBilling.android.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Plugin.InAppBilling/InAppBilling.android.cs b/src/Plugin.InAppBilling/InAppBilling.android.cs index 003728d..239dba3 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -218,8 +218,9 @@ public override async Task> GetPurchasesHistor _ => ProductType.Subs }; + var historyParams = QueryPurchaseHistoryParams.NewBuilder().SetProductType(skuType).Build(); //TODO: Binding needs updated - var purchasesResult = await BillingClient.QueryPurchaseHistoryAsync(skuType); + var purchasesResult = await BillingClient.QueryPurchaseHistoryAsync(historyParams); return purchasesResult?.PurchaseHistoryRecords?.Select(p => p.ToIABPurchase()) ?? new List(); @@ -269,7 +270,7 @@ async Task UpgradePurchasedSubscriptionInternalAsync(strin var updateParams = BillingFlowParams.SubscriptionUpdateParams.NewBuilder() .SetOldPurchaseToken(purchaseTokenOfOriginalSubscription) - .SetReplaceProrationMode((int)prorationMode) + .SetSubscriptionReplacementMode((int)prorationMode) .Build(); var t = skuDetails.GetSubscriptionOfferDetails()?.FirstOrDefault()?.OfferToken; From bfc546f650bb4afec7d815eb5a287d883e9f5245 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Mon, 22 Jul 2024 16:25:21 -0700 Subject: [PATCH 17/22] cleanup --- .../InAppBillingMauiTest.csproj | 2 +- .../Platforms/Android/AndroidManifest.xml | 2 +- .../Platforms/iOS/Program.cs | 1 + .../Plugin.InAppBilling.Classic.csproj | 97 ------------------- .../Plugin.InAppBilling.csproj | 50 ++++++---- 5 files changed, 35 insertions(+), 117 deletions(-) delete mode 100644 src/Plugin.InAppBilling/Plugin.InAppBilling.Classic.csproj diff --git a/src/InAppBillingTests/InAppBillingMauiTest/InAppBillingMauiTest.csproj b/src/InAppBillingTests/InAppBillingMauiTest/InAppBillingMauiTest.csproj index a858be9..2865d08 100644 --- a/src/InAppBillingTests/InAppBillingMauiTest/InAppBillingMauiTest.csproj +++ b/src/InAppBillingTests/InAppBillingMauiTest/InAppBillingMauiTest.csproj @@ -46,6 +46,6 @@ - + diff --git a/src/InAppBillingTests/InAppBillingMauiTest/Platforms/Android/AndroidManifest.xml b/src/InAppBillingTests/InAppBillingMauiTest/Platforms/Android/AndroidManifest.xml index 7570ff6..b17ace2 100644 --- a/src/InAppBillingTests/InAppBillingMauiTest/Platforms/Android/AndroidManifest.xml +++ b/src/InAppBillingTests/InAppBillingMauiTest/Platforms/Android/AndroidManifest.xml @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/src/InAppBillingTests/InAppBillingMauiTest/Platforms/iOS/Program.cs b/src/InAppBillingTests/InAppBillingMauiTest/Platforms/iOS/Program.cs index 12dc6d1..910c9ee 100644 --- a/src/InAppBillingTests/InAppBillingMauiTest/Platforms/iOS/Program.cs +++ b/src/InAppBillingTests/InAppBillingMauiTest/Platforms/iOS/Program.cs @@ -11,6 +11,7 @@ static void Main(string[] args) // if you want to use a different Application Delegate class from "AppDelegate" // you can specify it here. UIApplication.Main(args, null, typeof(AppDelegate)); + } } } \ No newline at end of file diff --git a/src/Plugin.InAppBilling/Plugin.InAppBilling.Classic.csproj b/src/Plugin.InAppBilling/Plugin.InAppBilling.Classic.csproj deleted file mode 100644 index 3b8e2cb..0000000 --- a/src/Plugin.InAppBilling/Plugin.InAppBilling.Classic.csproj +++ /dev/null @@ -1,97 +0,0 @@ - - - netstandard2.0;MonoAndroid10.0;Xamarin.iOS10;Xamarin.TVOS10;Xamarin.Mac20 - $(TargetFrameworks);uap10.0.16299; - Plugin.InAppBilling - Plugin.InAppBilling - $(AssemblyName) ($(TargetFramework)) - 1.0.0.0 - 1.0.0.0 - 1.0.0.0 - 9.0 - James Montemagno - Plugin.InAppBilling - true - true - https://raw.githubusercontent.com/jamesmontemagno/InAppBillingPlugin/master/art/icon_128.png - https://github.com/jamesmontemagno/InAppBillingPlugin/blob/master/LICENSE - JamesMontemagno - https://github.com/jamesmontemagno/InAppBillingPlugin - In-App Billing Plugin for iOS, Android, macOS, and Window - .net maui, maui, windows, macosxamarin, xamarin.forms, android, ios, uwp, windows phone, In-App Billing, purchases, plugin - In-App Billing Plugin for .NET MAUI, Xamarin, and Windows - - .NET MAUI, Xamarin, and Windows Plugin to In-App Billing. Get item information, purchase items, and restore purchases with a cross-platform API. - Read the full documenation on the projects page. - - Copyright 2022 - https://github.com/jamesmontemagno/InAppBillingPlugin - See: https://github.com/jamesmontemagno/InAppBillingPlugin - - en - default - false - $(DefineConstants); - - - full - true - false - - - true - portable - - - true - true - $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj index 3e682a9..51a00f0 100644 --- a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj +++ b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj @@ -1,7 +1,7 @@  net8.0-android;net8.0-ios;net8.0-maccatalyst;net8.0-tvos;net8.0-macos - $(TargetFrameworks);net8.0-windows10.0.19041; + $(TargetFrameworks);net8.0-windows10.0.19041.0 latest Plugin.InAppBilling Plugin.InAppBilling @@ -10,11 +10,14 @@ 7.0.0.0 7.0.0.0 James Montemagno + True Plugin.InAppBilling true true - https://raw.githubusercontent.com/jamesmontemagno/InAppBillingPlugin/master/art/icon_128.png - https://github.com/jamesmontemagno/InAppBillingPlugin/blob/master/LICENSE + True + true + true + snupkg JamesMontemagno https://github.com/jamesmontemagno/InAppBillingPlugin .NET MAUI, and Windows plugin to In-App Billing. @@ -27,17 +30,22 @@ Copyright 2024 https://github.com/jamesmontemagno/InAppBillingPlugin See: https://github.com/jamesmontemagno/InAppBillingPlugin - + MIT + True en false + icon.png + README.md - 10.0 - 21.0 - 10.0.17763.0 - 10.0.17763.0 + 14.2 + 14.0 + 13.0 + 21.0 + 10.0.17763.0 + 10.0.17763.0 @@ -49,42 +57,48 @@ true portable - + true true $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - + - - + + - + - + - + - + - + - + + + + + + + From a6aedbb5359fd5a3513172954cafd760c9fbf714 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Mon, 22 Jul 2024 17:07:14 -0700 Subject: [PATCH 18/22] cleanup --- .../InAppBillingMauiTest.csproj | 2 +- ...ppBilling.apple.cs => InAppBilling.ios.cs} | 0 .../InAppBilling.maccatalyst.cs | 968 ++++++++++++++++++ src/Plugin.InAppBilling/InAppBilling.macos.cs | 968 ++++++++++++++++++ .../Plugin.InAppBilling.csproj | 58 +- 5 files changed, 1987 insertions(+), 9 deletions(-) rename src/Plugin.InAppBilling/{InAppBilling.apple.cs => InAppBilling.ios.cs} (100%) create mode 100644 src/Plugin.InAppBilling/InAppBilling.maccatalyst.cs create mode 100644 src/Plugin.InAppBilling/InAppBilling.macos.cs diff --git a/src/InAppBillingTests/InAppBillingMauiTest/InAppBillingMauiTest.csproj b/src/InAppBillingTests/InAppBillingMauiTest/InAppBillingMauiTest.csproj index 2865d08..af26d81 100644 --- a/src/InAppBillingTests/InAppBillingMauiTest/InAppBillingMauiTest.csproj +++ b/src/InAppBillingTests/InAppBillingMauiTest/InAppBillingMauiTest.csproj @@ -1,7 +1,7 @@  - net8.0-android;net8.0-ios;net8.0-maccatalyst + net8.0-android;net8.0-ios $(TargetFrameworks);net8.0-windows10.0.19041 Exe InAppBillingMauiTest diff --git a/src/Plugin.InAppBilling/InAppBilling.apple.cs b/src/Plugin.InAppBilling/InAppBilling.ios.cs similarity index 100% rename from src/Plugin.InAppBilling/InAppBilling.apple.cs rename to src/Plugin.InAppBilling/InAppBilling.ios.cs diff --git a/src/Plugin.InAppBilling/InAppBilling.maccatalyst.cs b/src/Plugin.InAppBilling/InAppBilling.maccatalyst.cs new file mode 100644 index 0000000..98b1201 --- /dev/null +++ b/src/Plugin.InAppBilling/InAppBilling.maccatalyst.cs @@ -0,0 +1,968 @@ +using Foundation; +using StoreKit; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Plugin.InAppBilling +{ + /// + /// Implementation for InAppBilling + /// + [Preserve(AllMembers = true)] + public class InAppBillingImplementation : BaseInAppBilling + { + /// + /// Backwards compat flag that may be removed in the future to auto finish all transactions like in v4 + /// + public static bool FinishAllTransactions { get; set; } = true; + +#if __IOS__ || __TVOS__ + internal static bool HasIntroductoryOffer => UIKit.UIDevice.CurrentDevice.CheckSystemVersion(11, 2); + internal static bool HasProductDiscounts => UIKit.UIDevice.CurrentDevice.CheckSystemVersion(12, 2); + internal static bool HasSubscriptionGroupId => UIKit.UIDevice.CurrentDevice.CheckSystemVersion(12, 0); + internal static bool HasStorefront => UIKit.UIDevice.CurrentDevice.CheckSystemVersion(13, 0); + internal static bool HasFamilyShareable => UIKit.UIDevice.CurrentDevice.CheckSystemVersion(14, 0); +#else + static bool initIntro, hasIntro, initDiscounts, hasDiscounts, initFamily, hasFamily, initSubGroup, hasSubGroup, initStore, hasStore; + internal static bool HasIntroductoryOffer + { + get + { + if (initIntro) + return hasIntro; + + initIntro = true; + + + using var info = new NSProcessInfo(); + hasIntro = info.IsOperatingSystemAtLeastVersion(new NSOperatingSystemVersion(10,13,2)); + return hasIntro; + + } + } + internal static bool HasStorefront + { + get + { + if (initStore) + return hasStore; + + initStore = true; + + + using var info = new NSProcessInfo(); + hasStore = info.IsOperatingSystemAtLeastVersion(new NSOperatingSystemVersion(10, 15, 0)); + return hasStore; + + } + } + internal static bool HasProductDiscounts + { + get + { + if (initDiscounts) + return hasDiscounts; + + initDiscounts = true; + + + using var info = new NSProcessInfo(); + hasDiscounts = info.IsOperatingSystemAtLeastVersion(new NSOperatingSystemVersion(10,14,4)); + return hasDiscounts; + + } + } + + internal static bool HasSubscriptionGroupId + { + get + { + if (initSubGroup) + return hasSubGroup; + + initSubGroup = true; + + + using var info = new NSProcessInfo(); + hasSubGroup = info.IsOperatingSystemAtLeastVersion(new NSOperatingSystemVersion(10,14,0)); + return hasSubGroup; + + } + } + + internal static bool HasFamilyShareable + { + get + { + if (initFamily) + return hasFamily; + + initFamily = true; + + + using var info = new NSProcessInfo(); + hasFamily = info.IsOperatingSystemAtLeastVersion(new NSOperatingSystemVersion(11,0,0)); + return hasFamily; + + } + } +#endif + + + /// + /// iOS: Displays a sheet that enables users to redeem subscription offer codes that you configure in App Store Connect. + /// + public override void PresentCodeRedemption() + { +#if __IOS__ && !__MACCATALYST__ + if(HasFamilyShareable) + SKPaymentQueue.DefaultQueue.PresentCodeRedemptionSheet(); +#endif + } + + Storefront storefront; + /// + /// Returns representation of storefront on iOS 13+ + /// + public override Storefront Storefront => HasStorefront ? (storefront ??= new Storefront + { + CountryCode = SKPaymentQueue.DefaultQueue.Storefront.CountryCode, + Id = SKPaymentQueue.DefaultQueue.Storefront.Identifier + }) : null; + + /// + /// Gets if user can make payments + /// + public override bool CanMakePayments => SKPaymentQueue.CanMakePayments; + + /// + /// Gets or sets a callback for out of band purchases to complete. + /// + public static Action OnPurchaseComplete { get; set; } = null; + + + /// + /// Gets or sets a callback for out of band failures to complete. + /// + public static Action OnPurchaseFailure { get; set; } = null; + + /// + /// + /// + public static Func OnShouldAddStorePayment { get; set; } = null; + + /// + /// Default constructor for In App Billing on iOS + /// + public InAppBillingImplementation() + { + Init(); + } + + void Init() + { + if(paymentObserver != null) + return; + + paymentObserver = new PaymentObserver(OnPurchaseComplete, OnPurchaseFailure, OnShouldAddStorePayment); + SKPaymentQueue.DefaultQueue.AddTransactionObserver(paymentObserver); + } + + /// + /// Gets or sets if in testing mode. Only for UWP + /// + public override bool InTestingMode { get; set; } + + + /// + /// Get product information of a specific product + /// + /// Sku or Id of the product(s) + /// Type of product offering + /// + public async override Task> GetProductInfoAsync(ItemType itemType, string[] productIds, CancellationToken cancellationToken) + { + Init(); + var products = await GetProductAsync(productIds, cancellationToken); + + return products.Select(p => new InAppBillingProduct + { + LocalizedPrice = p.LocalizedPrice(), + MicrosPrice = (long)(p.Price.DoubleValue * 1000000d), + Name = p.LocalizedTitle, + ProductId = p.ProductIdentifier, + Description = p.LocalizedDescription, + CurrencyCode = p.PriceLocale?.CurrencyCode ?? string.Empty, + AppleExtras = new InAppBillingProductAppleExtras + { + IsFamilyShareable = HasFamilyShareable && p.IsFamilyShareable, + SubscriptionGroupId = HasSubscriptionGroupId ? p.SubscriptionGroupIdentifier : null, + SubscriptionPeriod = p.ToSubscriptionPeriod(), + IntroductoryOffer = HasIntroductoryOffer ? p.IntroductoryPrice?.ToProductDiscount() : null, + Discounts = HasProductDiscounts ? p.Discounts?.Select(s => s.ToProductDiscount()).ToList() ?? null : null + } + }); + } + + Task> GetProductAsync(string[] productId, CancellationToken cancellationToken) + { + var productIdentifiers = NSSet.MakeNSObjectSet(productId.Select(i => new NSString(i)).ToArray()); + + var productRequestDelegate = new ProductRequestDelegate(IgnoreInvalidProducts); + + //set up product request for in-app purchase + var productsRequest = new SKProductsRequest(productIdentifiers) + { + Delegate = productRequestDelegate // SKProductsRequestDelegate.ReceivedResponse + }; + using var _ = cancellationToken.Register(() => productsRequest.Cancel()); + productsRequest.Start(); + + return productRequestDelegate.WaitForResponse(); + } + + /// + /// Get app purchase + /// + /// + /// + public async override Task> GetPurchasesAsync(ItemType itemType, CancellationToken cancellationToken) + { + Init(); + var purchases = await RestoreAsync(cancellationToken); + + var comparer = new InAppBillingPurchaseComparer(); + return purchases + ?.Where(p => p != null) + ?.Select(p2 => p2.ToIABPurchase()) + ?.Distinct(comparer); + } + + + + Task RestoreAsync(CancellationToken cancellationToken) + { + var tcsTransaction = new TaskCompletionSource(); + + var allTransactions = new List(); + + var handler = new Action(transactions => + { + if (transactions == null) + { + if (allTransactions.Count == 0) + tcsTransaction.TrySetException(new InAppBillingPurchaseException(PurchaseError.RestoreFailed, "Restore Transactions Failed")); + else + tcsTransaction.TrySetResult(allTransactions.ToArray()); + } + else + { + allTransactions.AddRange(transactions); + tcsTransaction.TrySetResult(allTransactions.ToArray()); + } + }); + + try + { + using var _ = cancellationToken.Register(() => tcsTransaction.TrySetCanceled()); + paymentObserver.TransactionsRestored += handler; + + foreach (var trans in SKPaymentQueue.DefaultQueue.Transactions) + { + var original = FindOriginalTransaction(trans); + if (original == null) + continue; + + allTransactions.Add(original); + } + + // Start receiving restored transactions + SKPaymentQueue.DefaultQueue.RestoreCompletedTransactions(); + + return tcsTransaction.Task; + } + finally + { + paymentObserver.TransactionsRestored -= handler; + } + } + + + + static SKPaymentTransaction FindOriginalTransaction(SKPaymentTransaction transaction) + { + if (transaction == null) + return null; + + if (transaction.TransactionState == SKPaymentTransactionState.Purchased || + transaction.TransactionState == SKPaymentTransactionState.Purchasing) + return transaction; + + if (transaction.OriginalTransaction != null) + return FindOriginalTransaction(transaction.OriginalTransaction); + + return transaction; + + } + + + + + /// + /// Purchase a specific product or subscription + /// + /// Sku or ID of product + /// Type of product being requested + /// 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, string subOfferToken = null, CancellationToken cancellationToken = default) + { + Init(); + var p = await PurchaseAsync(productId, itemType, obfuscatedAccountId, cancellationToken); + + var reference = new DateTime(2001, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + + + var purchase = new InAppBillingPurchase + { + TransactionDateUtc = reference.AddSeconds(p.TransactionDate?.SecondsSinceReferenceDate ?? 0), + Id = p.TransactionIdentifier, + OriginalTransactionIdentifier = p.OriginalTransaction?.TransactionIdentifier, + TransactionIdentifier = p.TransactionIdentifier, + ProductId = p.Payment?.ProductIdentifier ?? string.Empty, + ProductIds = new string[] { p.Payment?.ProductIdentifier ?? string.Empty }, + State = p.GetPurchaseState(), + ApplicationUsername = p.Payment?.ApplicationUsername ?? string.Empty, +#if __IOS__ || __TVOS__ + PurchaseToken = p.TransactionReceipt?.GetBase64EncodedString(NSDataBase64EncodingOptions.None) ?? string.Empty +#endif + }; + + return purchase; + } + + + async Task PurchaseAsync(string productId, ItemType itemType, string applicationUserName, CancellationToken cancellationToken) + { + var tcsTransaction = new TaskCompletionSource(); + + var handler = new Action((tran, success) => + { + if (tran?.Payment == null) + return; + + // Only handle results from this request + if (productId != tran.Payment.ProductIdentifier) + return; + + if (success) + { + tcsTransaction.TrySetResult(tran); + return; + } + + var errorCode = tran?.Error?.Code ?? -1; + var description = tran?.Error?.LocalizedDescription ?? string.Empty; + var error = PurchaseError.GeneralError; + switch (errorCode) + { + case (int)SKError.PaymentCancelled: + error = PurchaseError.UserCancelled; + break; + case (int)SKError.PaymentInvalid: + error = PurchaseError.PaymentInvalid; + break; + case (int)SKError.PaymentNotAllowed: + error = PurchaseError.PaymentNotAllowed; + break; + case (int)SKError.ProductNotAvailable: + error = PurchaseError.ItemUnavailable; + break; + case (int)SKError.Unknown: + try + { + var underlyingError = tran?.Error?.UserInfo?["NSUnderlyingError"] as NSError; + error = underlyingError?.Code == 3038 ? PurchaseError.AppleTermsConditionsChanged : PurchaseError.GeneralError; + } + catch + { + error = PurchaseError.GeneralError; + } + break; + case (int)SKError.ClientInvalid: + error = PurchaseError.BillingUnavailable; + break; + } + + tcsTransaction.TrySetException(new InAppBillingPurchaseException(error, description)); + + }); + + try + { + using var _ = cancellationToken.Register(() => tcsTransaction.TrySetCanceled()); + paymentObserver.TransactionCompleted += handler; + + var products = await GetProductAsync(new[] { productId }, cancellationToken); + var product = products?.FirstOrDefault(); + if (product == null) + throw new InAppBillingPurchaseException(PurchaseError.InvalidProduct); + + if (string.IsNullOrWhiteSpace(applicationUserName)) + { + var payment = SKPayment.CreateFrom(product); + //var payment = SKPayment.CreateFrom((SKProduct)SKProduct.FromObject(new NSString(productId))); + + SKPaymentQueue.DefaultQueue.AddPayment(payment); + } + else + { + var payment = SKMutablePayment.PaymentWithProduct(product); + payment.ApplicationUsername = applicationUserName; + + SKPaymentQueue.DefaultQueue.AddPayment(payment); + } + + return await tcsTransaction.Task; + } + finally + { + paymentObserver.TransactionCompleted -= handler; + } + } + + /// + /// (iOS not supported) Apple store manages upgrades natively when subscriptions of the same group are purchased. + /// + /// iOS not supported + public override Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration, CancellationToken cancellationToken = default) => + throw new NotImplementedException("iOS not supported. Apple store manages upgrades natively when subscriptions of the same group are purchased."); + + + /// + /// gets receipt data from bundle + /// + public override string ReceiptData + { + get + { + // Get the receipt data for (server-side) validation. + // See: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573 + NSData receiptUrl = null; + if (NSBundle.MainBundle.AppStoreReceiptUrl != null) + receiptUrl = NSData.FromUrl(NSBundle.MainBundle.AppStoreReceiptUrl); + + return receiptUrl?.GetBase64EncodedString(NSDataBase64EncodingOptions.None); + } + } + + + /// + /// Consume a purchase with a purchase token. + /// + /// Id or Sku of product + /// Original Purchase Token + /// If consumed successful + /// If an error occurs during processing + public override async Task ConsumePurchaseAsync(string productId, string transactionIdentifier, CancellationToken cancellationToken) + { + var items = await FinalizePurchaseAsync(new [] { transactionIdentifier }, cancellationToken); + var item = items.FirstOrDefault(); + + return item.Success; + } + + + /// + /// + /// + /// + /// + public override async Task> FinalizePurchaseOfProductAsync(string[] productIds, CancellationToken cancellationToken) + { + var purchases = await RestoreAsync(cancellationToken); + + var items = new List<(string Id, bool Success)>(); + + + if (purchases == null) + { + return items; + } + + + foreach (var t in productIds) + { + if (string.IsNullOrWhiteSpace(t)) + { + items.Add((t, false)); + continue; + } + + + var transactions = purchases.Where(p => p.Payment?.ProductIdentifier == t); + + if ((transactions?.Count() ?? 0) == 0) + { + items.Add((t, false)); + continue; + } + + var success = true; + foreach (var transaction in transactions) + { + + try + { + SKPaymentQueue.DefaultQueue.FinishTransaction(transaction); + } + catch (Exception ex) + { + Debug.WriteLine("Unable to finish transaction: " + ex); + + success = false; + } + } + + + items.Add((t, success)); + } + + return items; + } + + /// + /// Finish a transaction manually + /// + /// + /// + /// + public async override Task> FinalizePurchaseAsync(string[] transactionIdentifier, CancellationToken cancellationToken) + { + var purchases = await RestoreAsync(cancellationToken); + + var items = new List<(string Id, bool Success)>(); + + + if (purchases == null) + { + return items; + } + + + foreach (var t in transactionIdentifier) + { + if (string.IsNullOrWhiteSpace(t)) + { + items.Add((t, false)); + continue; + } + + var transactions = purchases.Where(p => p.TransactionIdentifier == t); + + if ((transactions?.Count() ?? 0) == 0) + { + items.Add((t, false)); + continue; + } + + var success = true; + foreach (var transaction in transactions) + { + + try + { + SKPaymentQueue.DefaultQueue.FinishTransaction(transaction); + } + catch (Exception ex) + { + Debug.WriteLine("Unable to finish transaction: " + ex); + success = false; + } + } + + items.Add((t, success)); + } + + return items; + } + + PaymentObserver paymentObserver; + + + bool disposed = false; + + + /// + /// Dispose + /// + /// + public override void Dispose(bool disposing) + { + if (disposed) + { + base.Dispose(disposing); + return; + } + + disposed = true; + + if (!disposing) + { + base.Dispose(disposing); + return; + } + + if (paymentObserver != null) + { + SKPaymentQueue.DefaultQueue.RemoveTransactionObserver(paymentObserver); + paymentObserver.Dispose(); + paymentObserver = null; + } + + + base.Dispose(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() => + tcsResponse.Task; + + + [Export("request:didFailWithError:")] + public void RequestFailed(SKRequest request, NSError error) => + tcsResponse.TrySetException(new InAppBillingPurchaseException(PurchaseError.ProductRequestFailed, error.LocalizedDescription)); + + + public void ReceivedResponse(SKProductsRequest request, SKProductsResponse response) + { + 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) + { + tcsResponse.TrySetResult(product); + return; + } + } + } + + + [Preserve(AllMembers = true)] + class PaymentObserver : SKPaymentTransactionObserver + { + public event Action TransactionCompleted; + public event Action TransactionsRestored; + + readonly List restoredTransactions = new (); + readonly Action onPurchaseSuccess; + readonly Action onPurchaseFailure; + readonly Func onShouldAddStorePayment; + + public PaymentObserver(Action onPurchaseSuccess, Action onPurchaseFailure, Func onShouldAddStorePayment) + { + this.onPurchaseSuccess = onPurchaseSuccess; + this.onPurchaseFailure = onPurchaseFailure; + this.onShouldAddStorePayment = onShouldAddStorePayment; + } + + public override bool ShouldAddStorePayment(SKPaymentQueue queue, SKPayment payment, SKProduct product) => + onShouldAddStorePayment?.Invoke(queue, payment, product) ?? false; + + public override void UpdatedTransactions(SKPaymentQueue queue, SKPaymentTransaction[] transactions) + { + var rt = transactions.Where(pt => pt.TransactionState == SKPaymentTransactionState.Restored); + + // Add our restored transactions to the list + // We might still get more from the initial request so we won't raise the event until + // RestoreCompletedTransactionsFinished is called + if (rt?.Any() ?? false) + restoredTransactions.AddRange(rt); + + foreach (var transaction in transactions) + { + if (transaction?.TransactionState == null) + break; + + Debug.WriteLine($"Updated Transaction | {transaction.ToStatusString()}"); + + switch (transaction.TransactionState) + { + case SKPaymentTransactionState.Restored: + case SKPaymentTransactionState.Purchased: + TransactionCompleted?.Invoke(transaction, true); + + onPurchaseSuccess?.Invoke(transaction.ToIABPurchase()); + + if(InAppBillingImplementation.FinishAllTransactions) + SKPaymentQueue.DefaultQueue.FinishTransaction(transaction); + break; + case SKPaymentTransactionState.Failed: + TransactionCompleted?.Invoke(transaction, false); + onPurchaseFailure?.Invoke(transaction?.ToIABPurchase()); + + if (InAppBillingImplementation.FinishAllTransactions) + SKPaymentQueue.DefaultQueue.FinishTransaction(transaction); + break; + default: + break; + } + } + } + + public override void RestoreCompletedTransactionsFinished(SKPaymentQueue queue) + { + if (restoredTransactions == null) + return; + + // This is called after all restored transactions have hit UpdatedTransactions + // at this point we are done with the restore request so let's fire up the event + var allTransactions = restoredTransactions.ToArray(); + + // Clear out the list of incoming restore transactions for future requests + restoredTransactions.Clear(); + + TransactionsRestored?.Invoke(allTransactions); + + if (InAppBillingImplementation.FinishAllTransactions) + { + foreach (var transaction in allTransactions) + { + try + { + SKPaymentQueue.DefaultQueue.FinishTransaction(transaction); + } + catch(Exception ex) + { + Console.WriteLine(ex); + } + } + } + } + + // Failure, just fire with null + public override void RestoreCompletedTransactionsFailedWithError(SKPaymentQueue queue, NSError error) => + TransactionsRestored?.Invoke(null); + + } + + + + [Preserve(AllMembers = true)] + static class SKTransactionExtensions + { + public static string ToStatusString(this SKPaymentTransaction transaction) => + transaction?.ToIABPurchase()?.ToString() ?? string.Empty; + + + public static InAppBillingPurchase ToIABPurchase(this SKPaymentTransaction transaction) + { + var p = transaction?.OriginalTransaction ?? transaction; + + if (p == null) + return null; + +#if __IOS__ || __TVOS__ + var finalToken = p.TransactionReceipt?.GetBase64EncodedString(NSDataBase64EncodingOptions.None); + if (string.IsNullOrEmpty(finalToken)) + finalToken = transaction.TransactionReceipt?.GetBase64EncodedString(NSDataBase64EncodingOptions.None); + +#else + var finalToken = string.Empty; +#endif + return new InAppBillingPurchase + { + TransactionDateUtc = NSDateToDateTimeUtc(transaction.TransactionDate), + Id = p.TransactionIdentifier, + OriginalTransactionIdentifier = p.OriginalTransaction?.TransactionIdentifier, + TransactionIdentifier = p.TransactionIdentifier, + ProductId = p.Payment?.ProductIdentifier ?? string.Empty, + ProductIds = new string[] { p.Payment?.ProductIdentifier ?? string.Empty }, + State = p.GetPurchaseState(), + PurchaseToken = finalToken, + ApplicationUsername = p.Payment?.ApplicationUsername + }; + } + + static DateTime NSDateToDateTimeUtc(NSDate date) + { + var reference = new DateTime(2001, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + + return reference.AddSeconds(date?.SecondsSinceReferenceDate ?? 0); + } + + public static PurchaseState GetPurchaseState(this SKPaymentTransaction transaction) + { + + if (transaction?.TransactionState == null) + return PurchaseState.Unknown; + + switch (transaction.TransactionState) + { + case SKPaymentTransactionState.Restored: + return PurchaseState.Restored; + case SKPaymentTransactionState.Purchasing: + return PurchaseState.Purchasing; + case SKPaymentTransactionState.Purchased: + return PurchaseState.Purchased; + case SKPaymentTransactionState.Failed: + return PurchaseState.Failed; + case SKPaymentTransactionState.Deferred: + return PurchaseState.Deferred; + default: + break; + } + + return PurchaseState.Unknown; + } + } + + + [Preserve(AllMembers = true)] + static class SKProductExtension + { + + + /// + /// Use Apple's sample code for formatting a SKProduct price + /// https://developer.apple.com/library/ios/#DOCUMENTATION/StoreKit/Reference/SKProduct_Reference/Reference/Reference.html#//apple_ref/occ/instp/SKProduct/priceLocale + /// Objective-C version: + /// NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; + /// [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; + /// [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle]; + /// [numberFormatter setLocale:product.priceLocale]; + /// NSString *formattedString = [numberFormatter stringFromNumber:product.price]; + /// + public static string LocalizedPrice(this SKProduct product) + { + if (product?.PriceLocale == null) + return string.Empty; + + var formatter = new NSNumberFormatter() + { + FormatterBehavior = NSNumberFormatterBehavior.Version_10_4, + NumberStyle = NSNumberFormatterStyle.Currency, + Locale = product.PriceLocale + }; + var formattedString = formatter.StringFromNumber(product.Price); + Console.WriteLine(" ** formatter.StringFromNumber(" + product.Price + ") = " + formattedString + " for locale " + product.PriceLocale.LocaleIdentifier); + return formattedString; + } + + public static SubscriptionPeriod ToSubscriptionPeriod(this SKProduct p) + { + if (!InAppBillingImplementation.HasIntroductoryOffer) + return null; + + if (p?.SubscriptionPeriod?.Unit == null) + return null; + + var subPeriod = new SubscriptionPeriod(); + + subPeriod.Unit = p.SubscriptionPeriod.Unit switch + { + SKProductPeriodUnit.Day => SubscriptionPeriodUnit.Day, + SKProductPeriodUnit.Month => SubscriptionPeriodUnit.Month, + SKProductPeriodUnit.Year => SubscriptionPeriodUnit.Year, + SKProductPeriodUnit.Week => SubscriptionPeriodUnit.Week, + _ => SubscriptionPeriodUnit.Unknown, + }; + + subPeriod.NumberOfUnits = (int)p.SubscriptionPeriod.NumberOfUnits; + + return subPeriod; + } + + public static InAppBillingProductDiscount ToProductDiscount(this SKProductDiscount pd) + { + if (!InAppBillingImplementation.HasIntroductoryOffer) + return null; + + if (pd == null) + return null; + + + var discount = new InAppBillingProductDiscount + { + LocalizedPrice = pd.LocalizedPrice(), + Price = (pd.Price?.DoubleValue ?? 0) * 1000000d, + NumberOfPeriods = (int)pd.NumberOfPeriods, + CurrencyCode = pd.PriceLocale?.CurrencyCode ?? string.Empty + }; + + discount.SubscriptionPeriod.NumberOfUnits = (int)pd.SubscriptionPeriod.NumberOfUnits; + + discount.SubscriptionPeriod.Unit = pd.SubscriptionPeriod.Unit switch + { + SKProductPeriodUnit.Day => SubscriptionPeriodUnit.Day, + SKProductPeriodUnit.Month => SubscriptionPeriodUnit.Month, + SKProductPeriodUnit.Year => SubscriptionPeriodUnit.Year, + SKProductPeriodUnit.Week => SubscriptionPeriodUnit.Week, + _ => SubscriptionPeriodUnit.Unknown + }; + + discount.PaymentMode = pd.PaymentMode switch + { + SKProductDiscountPaymentMode.FreeTrial => PaymentMode.FreeTrial, + SKProductDiscountPaymentMode.PayUpFront => PaymentMode.PayUpFront, + SKProductDiscountPaymentMode.PayAsYouGo => PaymentMode.PayAsYouGo, + _ => PaymentMode.Unknown, + }; + + if(InAppBillingImplementation.HasProductDiscounts) + { + discount.Id = pd.Identifier; + discount.Type = pd.Type switch + { + SKProductDiscountType.Introductory => ProductDiscountType.Introductory, + SKProductDiscountType.Subscription => ProductDiscountType.Subscription, + _ => ProductDiscountType.Unknown, + }; + } + + return discount; + } + + public static string LocalizedPrice(this SKProductDiscount product) + { + if (product?.PriceLocale == null) + return string.Empty; + + var formatter = new NSNumberFormatter() + { + FormatterBehavior = NSNumberFormatterBehavior.Version_10_4, + NumberStyle = NSNumberFormatterStyle.Currency, + Locale = product.PriceLocale + }; + var formattedString = formatter.StringFromNumber(product.Price); + Console.WriteLine(" ** formatter.StringFromNumber(" + product.Price + ") = " + formattedString + " for locale " + product.PriceLocale.LocaleIdentifier); + return formattedString; + } + } +} diff --git a/src/Plugin.InAppBilling/InAppBilling.macos.cs b/src/Plugin.InAppBilling/InAppBilling.macos.cs new file mode 100644 index 0000000..98b1201 --- /dev/null +++ b/src/Plugin.InAppBilling/InAppBilling.macos.cs @@ -0,0 +1,968 @@ +using Foundation; +using StoreKit; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Plugin.InAppBilling +{ + /// + /// Implementation for InAppBilling + /// + [Preserve(AllMembers = true)] + public class InAppBillingImplementation : BaseInAppBilling + { + /// + /// Backwards compat flag that may be removed in the future to auto finish all transactions like in v4 + /// + public static bool FinishAllTransactions { get; set; } = true; + +#if __IOS__ || __TVOS__ + internal static bool HasIntroductoryOffer => UIKit.UIDevice.CurrentDevice.CheckSystemVersion(11, 2); + internal static bool HasProductDiscounts => UIKit.UIDevice.CurrentDevice.CheckSystemVersion(12, 2); + internal static bool HasSubscriptionGroupId => UIKit.UIDevice.CurrentDevice.CheckSystemVersion(12, 0); + internal static bool HasStorefront => UIKit.UIDevice.CurrentDevice.CheckSystemVersion(13, 0); + internal static bool HasFamilyShareable => UIKit.UIDevice.CurrentDevice.CheckSystemVersion(14, 0); +#else + static bool initIntro, hasIntro, initDiscounts, hasDiscounts, initFamily, hasFamily, initSubGroup, hasSubGroup, initStore, hasStore; + internal static bool HasIntroductoryOffer + { + get + { + if (initIntro) + return hasIntro; + + initIntro = true; + + + using var info = new NSProcessInfo(); + hasIntro = info.IsOperatingSystemAtLeastVersion(new NSOperatingSystemVersion(10,13,2)); + return hasIntro; + + } + } + internal static bool HasStorefront + { + get + { + if (initStore) + return hasStore; + + initStore = true; + + + using var info = new NSProcessInfo(); + hasStore = info.IsOperatingSystemAtLeastVersion(new NSOperatingSystemVersion(10, 15, 0)); + return hasStore; + + } + } + internal static bool HasProductDiscounts + { + get + { + if (initDiscounts) + return hasDiscounts; + + initDiscounts = true; + + + using var info = new NSProcessInfo(); + hasDiscounts = info.IsOperatingSystemAtLeastVersion(new NSOperatingSystemVersion(10,14,4)); + return hasDiscounts; + + } + } + + internal static bool HasSubscriptionGroupId + { + get + { + if (initSubGroup) + return hasSubGroup; + + initSubGroup = true; + + + using var info = new NSProcessInfo(); + hasSubGroup = info.IsOperatingSystemAtLeastVersion(new NSOperatingSystemVersion(10,14,0)); + return hasSubGroup; + + } + } + + internal static bool HasFamilyShareable + { + get + { + if (initFamily) + return hasFamily; + + initFamily = true; + + + using var info = new NSProcessInfo(); + hasFamily = info.IsOperatingSystemAtLeastVersion(new NSOperatingSystemVersion(11,0,0)); + return hasFamily; + + } + } +#endif + + + /// + /// iOS: Displays a sheet that enables users to redeem subscription offer codes that you configure in App Store Connect. + /// + public override void PresentCodeRedemption() + { +#if __IOS__ && !__MACCATALYST__ + if(HasFamilyShareable) + SKPaymentQueue.DefaultQueue.PresentCodeRedemptionSheet(); +#endif + } + + Storefront storefront; + /// + /// Returns representation of storefront on iOS 13+ + /// + public override Storefront Storefront => HasStorefront ? (storefront ??= new Storefront + { + CountryCode = SKPaymentQueue.DefaultQueue.Storefront.CountryCode, + Id = SKPaymentQueue.DefaultQueue.Storefront.Identifier + }) : null; + + /// + /// Gets if user can make payments + /// + public override bool CanMakePayments => SKPaymentQueue.CanMakePayments; + + /// + /// Gets or sets a callback for out of band purchases to complete. + /// + public static Action OnPurchaseComplete { get; set; } = null; + + + /// + /// Gets or sets a callback for out of band failures to complete. + /// + public static Action OnPurchaseFailure { get; set; } = null; + + /// + /// + /// + public static Func OnShouldAddStorePayment { get; set; } = null; + + /// + /// Default constructor for In App Billing on iOS + /// + public InAppBillingImplementation() + { + Init(); + } + + void Init() + { + if(paymentObserver != null) + return; + + paymentObserver = new PaymentObserver(OnPurchaseComplete, OnPurchaseFailure, OnShouldAddStorePayment); + SKPaymentQueue.DefaultQueue.AddTransactionObserver(paymentObserver); + } + + /// + /// Gets or sets if in testing mode. Only for UWP + /// + public override bool InTestingMode { get; set; } + + + /// + /// Get product information of a specific product + /// + /// Sku or Id of the product(s) + /// Type of product offering + /// + public async override Task> GetProductInfoAsync(ItemType itemType, string[] productIds, CancellationToken cancellationToken) + { + Init(); + var products = await GetProductAsync(productIds, cancellationToken); + + return products.Select(p => new InAppBillingProduct + { + LocalizedPrice = p.LocalizedPrice(), + MicrosPrice = (long)(p.Price.DoubleValue * 1000000d), + Name = p.LocalizedTitle, + ProductId = p.ProductIdentifier, + Description = p.LocalizedDescription, + CurrencyCode = p.PriceLocale?.CurrencyCode ?? string.Empty, + AppleExtras = new InAppBillingProductAppleExtras + { + IsFamilyShareable = HasFamilyShareable && p.IsFamilyShareable, + SubscriptionGroupId = HasSubscriptionGroupId ? p.SubscriptionGroupIdentifier : null, + SubscriptionPeriod = p.ToSubscriptionPeriod(), + IntroductoryOffer = HasIntroductoryOffer ? p.IntroductoryPrice?.ToProductDiscount() : null, + Discounts = HasProductDiscounts ? p.Discounts?.Select(s => s.ToProductDiscount()).ToList() ?? null : null + } + }); + } + + Task> GetProductAsync(string[] productId, CancellationToken cancellationToken) + { + var productIdentifiers = NSSet.MakeNSObjectSet(productId.Select(i => new NSString(i)).ToArray()); + + var productRequestDelegate = new ProductRequestDelegate(IgnoreInvalidProducts); + + //set up product request for in-app purchase + var productsRequest = new SKProductsRequest(productIdentifiers) + { + Delegate = productRequestDelegate // SKProductsRequestDelegate.ReceivedResponse + }; + using var _ = cancellationToken.Register(() => productsRequest.Cancel()); + productsRequest.Start(); + + return productRequestDelegate.WaitForResponse(); + } + + /// + /// Get app purchase + /// + /// + /// + public async override Task> GetPurchasesAsync(ItemType itemType, CancellationToken cancellationToken) + { + Init(); + var purchases = await RestoreAsync(cancellationToken); + + var comparer = new InAppBillingPurchaseComparer(); + return purchases + ?.Where(p => p != null) + ?.Select(p2 => p2.ToIABPurchase()) + ?.Distinct(comparer); + } + + + + Task RestoreAsync(CancellationToken cancellationToken) + { + var tcsTransaction = new TaskCompletionSource(); + + var allTransactions = new List(); + + var handler = new Action(transactions => + { + if (transactions == null) + { + if (allTransactions.Count == 0) + tcsTransaction.TrySetException(new InAppBillingPurchaseException(PurchaseError.RestoreFailed, "Restore Transactions Failed")); + else + tcsTransaction.TrySetResult(allTransactions.ToArray()); + } + else + { + allTransactions.AddRange(transactions); + tcsTransaction.TrySetResult(allTransactions.ToArray()); + } + }); + + try + { + using var _ = cancellationToken.Register(() => tcsTransaction.TrySetCanceled()); + paymentObserver.TransactionsRestored += handler; + + foreach (var trans in SKPaymentQueue.DefaultQueue.Transactions) + { + var original = FindOriginalTransaction(trans); + if (original == null) + continue; + + allTransactions.Add(original); + } + + // Start receiving restored transactions + SKPaymentQueue.DefaultQueue.RestoreCompletedTransactions(); + + return tcsTransaction.Task; + } + finally + { + paymentObserver.TransactionsRestored -= handler; + } + } + + + + static SKPaymentTransaction FindOriginalTransaction(SKPaymentTransaction transaction) + { + if (transaction == null) + return null; + + if (transaction.TransactionState == SKPaymentTransactionState.Purchased || + transaction.TransactionState == SKPaymentTransactionState.Purchasing) + return transaction; + + if (transaction.OriginalTransaction != null) + return FindOriginalTransaction(transaction.OriginalTransaction); + + return transaction; + + } + + + + + /// + /// Purchase a specific product or subscription + /// + /// Sku or ID of product + /// Type of product being requested + /// 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, string subOfferToken = null, CancellationToken cancellationToken = default) + { + Init(); + var p = await PurchaseAsync(productId, itemType, obfuscatedAccountId, cancellationToken); + + var reference = new DateTime(2001, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + + + var purchase = new InAppBillingPurchase + { + TransactionDateUtc = reference.AddSeconds(p.TransactionDate?.SecondsSinceReferenceDate ?? 0), + Id = p.TransactionIdentifier, + OriginalTransactionIdentifier = p.OriginalTransaction?.TransactionIdentifier, + TransactionIdentifier = p.TransactionIdentifier, + ProductId = p.Payment?.ProductIdentifier ?? string.Empty, + ProductIds = new string[] { p.Payment?.ProductIdentifier ?? string.Empty }, + State = p.GetPurchaseState(), + ApplicationUsername = p.Payment?.ApplicationUsername ?? string.Empty, +#if __IOS__ || __TVOS__ + PurchaseToken = p.TransactionReceipt?.GetBase64EncodedString(NSDataBase64EncodingOptions.None) ?? string.Empty +#endif + }; + + return purchase; + } + + + async Task PurchaseAsync(string productId, ItemType itemType, string applicationUserName, CancellationToken cancellationToken) + { + var tcsTransaction = new TaskCompletionSource(); + + var handler = new Action((tran, success) => + { + if (tran?.Payment == null) + return; + + // Only handle results from this request + if (productId != tran.Payment.ProductIdentifier) + return; + + if (success) + { + tcsTransaction.TrySetResult(tran); + return; + } + + var errorCode = tran?.Error?.Code ?? -1; + var description = tran?.Error?.LocalizedDescription ?? string.Empty; + var error = PurchaseError.GeneralError; + switch (errorCode) + { + case (int)SKError.PaymentCancelled: + error = PurchaseError.UserCancelled; + break; + case (int)SKError.PaymentInvalid: + error = PurchaseError.PaymentInvalid; + break; + case (int)SKError.PaymentNotAllowed: + error = PurchaseError.PaymentNotAllowed; + break; + case (int)SKError.ProductNotAvailable: + error = PurchaseError.ItemUnavailable; + break; + case (int)SKError.Unknown: + try + { + var underlyingError = tran?.Error?.UserInfo?["NSUnderlyingError"] as NSError; + error = underlyingError?.Code == 3038 ? PurchaseError.AppleTermsConditionsChanged : PurchaseError.GeneralError; + } + catch + { + error = PurchaseError.GeneralError; + } + break; + case (int)SKError.ClientInvalid: + error = PurchaseError.BillingUnavailable; + break; + } + + tcsTransaction.TrySetException(new InAppBillingPurchaseException(error, description)); + + }); + + try + { + using var _ = cancellationToken.Register(() => tcsTransaction.TrySetCanceled()); + paymentObserver.TransactionCompleted += handler; + + var products = await GetProductAsync(new[] { productId }, cancellationToken); + var product = products?.FirstOrDefault(); + if (product == null) + throw new InAppBillingPurchaseException(PurchaseError.InvalidProduct); + + if (string.IsNullOrWhiteSpace(applicationUserName)) + { + var payment = SKPayment.CreateFrom(product); + //var payment = SKPayment.CreateFrom((SKProduct)SKProduct.FromObject(new NSString(productId))); + + SKPaymentQueue.DefaultQueue.AddPayment(payment); + } + else + { + var payment = SKMutablePayment.PaymentWithProduct(product); + payment.ApplicationUsername = applicationUserName; + + SKPaymentQueue.DefaultQueue.AddPayment(payment); + } + + return await tcsTransaction.Task; + } + finally + { + paymentObserver.TransactionCompleted -= handler; + } + } + + /// + /// (iOS not supported) Apple store manages upgrades natively when subscriptions of the same group are purchased. + /// + /// iOS not supported + public override Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration, CancellationToken cancellationToken = default) => + throw new NotImplementedException("iOS not supported. Apple store manages upgrades natively when subscriptions of the same group are purchased."); + + + /// + /// gets receipt data from bundle + /// + public override string ReceiptData + { + get + { + // Get the receipt data for (server-side) validation. + // See: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573 + NSData receiptUrl = null; + if (NSBundle.MainBundle.AppStoreReceiptUrl != null) + receiptUrl = NSData.FromUrl(NSBundle.MainBundle.AppStoreReceiptUrl); + + return receiptUrl?.GetBase64EncodedString(NSDataBase64EncodingOptions.None); + } + } + + + /// + /// Consume a purchase with a purchase token. + /// + /// Id or Sku of product + /// Original Purchase Token + /// If consumed successful + /// If an error occurs during processing + public override async Task ConsumePurchaseAsync(string productId, string transactionIdentifier, CancellationToken cancellationToken) + { + var items = await FinalizePurchaseAsync(new [] { transactionIdentifier }, cancellationToken); + var item = items.FirstOrDefault(); + + return item.Success; + } + + + /// + /// + /// + /// + /// + public override async Task> FinalizePurchaseOfProductAsync(string[] productIds, CancellationToken cancellationToken) + { + var purchases = await RestoreAsync(cancellationToken); + + var items = new List<(string Id, bool Success)>(); + + + if (purchases == null) + { + return items; + } + + + foreach (var t in productIds) + { + if (string.IsNullOrWhiteSpace(t)) + { + items.Add((t, false)); + continue; + } + + + var transactions = purchases.Where(p => p.Payment?.ProductIdentifier == t); + + if ((transactions?.Count() ?? 0) == 0) + { + items.Add((t, false)); + continue; + } + + var success = true; + foreach (var transaction in transactions) + { + + try + { + SKPaymentQueue.DefaultQueue.FinishTransaction(transaction); + } + catch (Exception ex) + { + Debug.WriteLine("Unable to finish transaction: " + ex); + + success = false; + } + } + + + items.Add((t, success)); + } + + return items; + } + + /// + /// Finish a transaction manually + /// + /// + /// + /// + public async override Task> FinalizePurchaseAsync(string[] transactionIdentifier, CancellationToken cancellationToken) + { + var purchases = await RestoreAsync(cancellationToken); + + var items = new List<(string Id, bool Success)>(); + + + if (purchases == null) + { + return items; + } + + + foreach (var t in transactionIdentifier) + { + if (string.IsNullOrWhiteSpace(t)) + { + items.Add((t, false)); + continue; + } + + var transactions = purchases.Where(p => p.TransactionIdentifier == t); + + if ((transactions?.Count() ?? 0) == 0) + { + items.Add((t, false)); + continue; + } + + var success = true; + foreach (var transaction in transactions) + { + + try + { + SKPaymentQueue.DefaultQueue.FinishTransaction(transaction); + } + catch (Exception ex) + { + Debug.WriteLine("Unable to finish transaction: " + ex); + success = false; + } + } + + items.Add((t, success)); + } + + return items; + } + + PaymentObserver paymentObserver; + + + bool disposed = false; + + + /// + /// Dispose + /// + /// + public override void Dispose(bool disposing) + { + if (disposed) + { + base.Dispose(disposing); + return; + } + + disposed = true; + + if (!disposing) + { + base.Dispose(disposing); + return; + } + + if (paymentObserver != null) + { + SKPaymentQueue.DefaultQueue.RemoveTransactionObserver(paymentObserver); + paymentObserver.Dispose(); + paymentObserver = null; + } + + + base.Dispose(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() => + tcsResponse.Task; + + + [Export("request:didFailWithError:")] + public void RequestFailed(SKRequest request, NSError error) => + tcsResponse.TrySetException(new InAppBillingPurchaseException(PurchaseError.ProductRequestFailed, error.LocalizedDescription)); + + + public void ReceivedResponse(SKProductsRequest request, SKProductsResponse response) + { + 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) + { + tcsResponse.TrySetResult(product); + return; + } + } + } + + + [Preserve(AllMembers = true)] + class PaymentObserver : SKPaymentTransactionObserver + { + public event Action TransactionCompleted; + public event Action TransactionsRestored; + + readonly List restoredTransactions = new (); + readonly Action onPurchaseSuccess; + readonly Action onPurchaseFailure; + readonly Func onShouldAddStorePayment; + + public PaymentObserver(Action onPurchaseSuccess, Action onPurchaseFailure, Func onShouldAddStorePayment) + { + this.onPurchaseSuccess = onPurchaseSuccess; + this.onPurchaseFailure = onPurchaseFailure; + this.onShouldAddStorePayment = onShouldAddStorePayment; + } + + public override bool ShouldAddStorePayment(SKPaymentQueue queue, SKPayment payment, SKProduct product) => + onShouldAddStorePayment?.Invoke(queue, payment, product) ?? false; + + public override void UpdatedTransactions(SKPaymentQueue queue, SKPaymentTransaction[] transactions) + { + var rt = transactions.Where(pt => pt.TransactionState == SKPaymentTransactionState.Restored); + + // Add our restored transactions to the list + // We might still get more from the initial request so we won't raise the event until + // RestoreCompletedTransactionsFinished is called + if (rt?.Any() ?? false) + restoredTransactions.AddRange(rt); + + foreach (var transaction in transactions) + { + if (transaction?.TransactionState == null) + break; + + Debug.WriteLine($"Updated Transaction | {transaction.ToStatusString()}"); + + switch (transaction.TransactionState) + { + case SKPaymentTransactionState.Restored: + case SKPaymentTransactionState.Purchased: + TransactionCompleted?.Invoke(transaction, true); + + onPurchaseSuccess?.Invoke(transaction.ToIABPurchase()); + + if(InAppBillingImplementation.FinishAllTransactions) + SKPaymentQueue.DefaultQueue.FinishTransaction(transaction); + break; + case SKPaymentTransactionState.Failed: + TransactionCompleted?.Invoke(transaction, false); + onPurchaseFailure?.Invoke(transaction?.ToIABPurchase()); + + if (InAppBillingImplementation.FinishAllTransactions) + SKPaymentQueue.DefaultQueue.FinishTransaction(transaction); + break; + default: + break; + } + } + } + + public override void RestoreCompletedTransactionsFinished(SKPaymentQueue queue) + { + if (restoredTransactions == null) + return; + + // This is called after all restored transactions have hit UpdatedTransactions + // at this point we are done with the restore request so let's fire up the event + var allTransactions = restoredTransactions.ToArray(); + + // Clear out the list of incoming restore transactions for future requests + restoredTransactions.Clear(); + + TransactionsRestored?.Invoke(allTransactions); + + if (InAppBillingImplementation.FinishAllTransactions) + { + foreach (var transaction in allTransactions) + { + try + { + SKPaymentQueue.DefaultQueue.FinishTransaction(transaction); + } + catch(Exception ex) + { + Console.WriteLine(ex); + } + } + } + } + + // Failure, just fire with null + public override void RestoreCompletedTransactionsFailedWithError(SKPaymentQueue queue, NSError error) => + TransactionsRestored?.Invoke(null); + + } + + + + [Preserve(AllMembers = true)] + static class SKTransactionExtensions + { + public static string ToStatusString(this SKPaymentTransaction transaction) => + transaction?.ToIABPurchase()?.ToString() ?? string.Empty; + + + public static InAppBillingPurchase ToIABPurchase(this SKPaymentTransaction transaction) + { + var p = transaction?.OriginalTransaction ?? transaction; + + if (p == null) + return null; + +#if __IOS__ || __TVOS__ + var finalToken = p.TransactionReceipt?.GetBase64EncodedString(NSDataBase64EncodingOptions.None); + if (string.IsNullOrEmpty(finalToken)) + finalToken = transaction.TransactionReceipt?.GetBase64EncodedString(NSDataBase64EncodingOptions.None); + +#else + var finalToken = string.Empty; +#endif + return new InAppBillingPurchase + { + TransactionDateUtc = NSDateToDateTimeUtc(transaction.TransactionDate), + Id = p.TransactionIdentifier, + OriginalTransactionIdentifier = p.OriginalTransaction?.TransactionIdentifier, + TransactionIdentifier = p.TransactionIdentifier, + ProductId = p.Payment?.ProductIdentifier ?? string.Empty, + ProductIds = new string[] { p.Payment?.ProductIdentifier ?? string.Empty }, + State = p.GetPurchaseState(), + PurchaseToken = finalToken, + ApplicationUsername = p.Payment?.ApplicationUsername + }; + } + + static DateTime NSDateToDateTimeUtc(NSDate date) + { + var reference = new DateTime(2001, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + + return reference.AddSeconds(date?.SecondsSinceReferenceDate ?? 0); + } + + public static PurchaseState GetPurchaseState(this SKPaymentTransaction transaction) + { + + if (transaction?.TransactionState == null) + return PurchaseState.Unknown; + + switch (transaction.TransactionState) + { + case SKPaymentTransactionState.Restored: + return PurchaseState.Restored; + case SKPaymentTransactionState.Purchasing: + return PurchaseState.Purchasing; + case SKPaymentTransactionState.Purchased: + return PurchaseState.Purchased; + case SKPaymentTransactionState.Failed: + return PurchaseState.Failed; + case SKPaymentTransactionState.Deferred: + return PurchaseState.Deferred; + default: + break; + } + + return PurchaseState.Unknown; + } + } + + + [Preserve(AllMembers = true)] + static class SKProductExtension + { + + + /// + /// Use Apple's sample code for formatting a SKProduct price + /// https://developer.apple.com/library/ios/#DOCUMENTATION/StoreKit/Reference/SKProduct_Reference/Reference/Reference.html#//apple_ref/occ/instp/SKProduct/priceLocale + /// Objective-C version: + /// NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; + /// [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; + /// [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle]; + /// [numberFormatter setLocale:product.priceLocale]; + /// NSString *formattedString = [numberFormatter stringFromNumber:product.price]; + /// + public static string LocalizedPrice(this SKProduct product) + { + if (product?.PriceLocale == null) + return string.Empty; + + var formatter = new NSNumberFormatter() + { + FormatterBehavior = NSNumberFormatterBehavior.Version_10_4, + NumberStyle = NSNumberFormatterStyle.Currency, + Locale = product.PriceLocale + }; + var formattedString = formatter.StringFromNumber(product.Price); + Console.WriteLine(" ** formatter.StringFromNumber(" + product.Price + ") = " + formattedString + " for locale " + product.PriceLocale.LocaleIdentifier); + return formattedString; + } + + public static SubscriptionPeriod ToSubscriptionPeriod(this SKProduct p) + { + if (!InAppBillingImplementation.HasIntroductoryOffer) + return null; + + if (p?.SubscriptionPeriod?.Unit == null) + return null; + + var subPeriod = new SubscriptionPeriod(); + + subPeriod.Unit = p.SubscriptionPeriod.Unit switch + { + SKProductPeriodUnit.Day => SubscriptionPeriodUnit.Day, + SKProductPeriodUnit.Month => SubscriptionPeriodUnit.Month, + SKProductPeriodUnit.Year => SubscriptionPeriodUnit.Year, + SKProductPeriodUnit.Week => SubscriptionPeriodUnit.Week, + _ => SubscriptionPeriodUnit.Unknown, + }; + + subPeriod.NumberOfUnits = (int)p.SubscriptionPeriod.NumberOfUnits; + + return subPeriod; + } + + public static InAppBillingProductDiscount ToProductDiscount(this SKProductDiscount pd) + { + if (!InAppBillingImplementation.HasIntroductoryOffer) + return null; + + if (pd == null) + return null; + + + var discount = new InAppBillingProductDiscount + { + LocalizedPrice = pd.LocalizedPrice(), + Price = (pd.Price?.DoubleValue ?? 0) * 1000000d, + NumberOfPeriods = (int)pd.NumberOfPeriods, + CurrencyCode = pd.PriceLocale?.CurrencyCode ?? string.Empty + }; + + discount.SubscriptionPeriod.NumberOfUnits = (int)pd.SubscriptionPeriod.NumberOfUnits; + + discount.SubscriptionPeriod.Unit = pd.SubscriptionPeriod.Unit switch + { + SKProductPeriodUnit.Day => SubscriptionPeriodUnit.Day, + SKProductPeriodUnit.Month => SubscriptionPeriodUnit.Month, + SKProductPeriodUnit.Year => SubscriptionPeriodUnit.Year, + SKProductPeriodUnit.Week => SubscriptionPeriodUnit.Week, + _ => SubscriptionPeriodUnit.Unknown + }; + + discount.PaymentMode = pd.PaymentMode switch + { + SKProductDiscountPaymentMode.FreeTrial => PaymentMode.FreeTrial, + SKProductDiscountPaymentMode.PayUpFront => PaymentMode.PayUpFront, + SKProductDiscountPaymentMode.PayAsYouGo => PaymentMode.PayAsYouGo, + _ => PaymentMode.Unknown, + }; + + if(InAppBillingImplementation.HasProductDiscounts) + { + discount.Id = pd.Identifier; + discount.Type = pd.Type switch + { + SKProductDiscountType.Introductory => ProductDiscountType.Introductory, + SKProductDiscountType.Subscription => ProductDiscountType.Subscription, + _ => ProductDiscountType.Unknown, + }; + } + + return discount; + } + + public static string LocalizedPrice(this SKProductDiscount product) + { + if (product?.PriceLocale == null) + return string.Empty; + + var formatter = new NSNumberFormatter() + { + FormatterBehavior = NSNumberFormatterBehavior.Version_10_4, + NumberStyle = NSNumberFormatterStyle.Currency, + Locale = product.PriceLocale + }; + var formattedString = formatter.StringFromNumber(product.Price); + Console.WriteLine(" ** formatter.StringFromNumber(" + product.Price + ") = " + formattedString + " for locale " + product.PriceLocale.LocaleIdentifier); + return formattedString; + } + } +} diff --git a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj index 51a00f0..d55d196 100644 --- a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj +++ b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj @@ -1,8 +1,11 @@  - net8.0-android;net8.0-ios;net8.0-maccatalyst;net8.0-tvos;net8.0-macos + net8.0-android;net8.0-ios;net8.0-maccatalyst;net8.0-macos $(TargetFrameworks);net8.0-windows10.0.19041.0 latest + true + true + true Plugin.InAppBilling Plugin.InAppBilling $(AssemblyName) ($(TargetFramework)) @@ -33,9 +36,10 @@ MIT True en - false + true icon.png README.md + true @@ -62,6 +66,7 @@ true $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + @@ -69,18 +74,55 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + --> - + From 633c1fb590f6c7522e5b6a66edee294b20962155 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Tue, 23 Jul 2024 14:46:26 -0700 Subject: [PATCH 19/22] Update Plugin.InAppBilling.csproj --- .../Plugin.InAppBilling.csproj | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj index d55d196..7d6de8d 100644 --- a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj +++ b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj @@ -96,6 +96,33 @@ + + 1.9.0.4 + + + 1.9.0.4 + + + 1.4.0.6 + + + 1.4.0.5 + + + 2.8.3.1 + + + 2.8.3.1 + + + 2.8.3.1 + + + 2.8.3.1 + + + 2.8.3.1 + From 1bd5ea0238ad173d4faa8a947e87061944764e64 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Fri, 9 Aug 2024 09:41:19 -0700 Subject: [PATCH 20/22] Updates for RestoreAsync --- src/Plugin.InAppBilling/InAppBilling.ios.cs | 5 +++-- src/Plugin.InAppBilling/InAppBilling.maccatalyst.cs | 5 +++-- src/Plugin.InAppBilling/InAppBilling.macos.cs | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Plugin.InAppBilling/InAppBilling.ios.cs b/src/Plugin.InAppBilling/InAppBilling.ios.cs index 98b1201..d891880 100644 --- a/src/Plugin.InAppBilling/InAppBilling.ios.cs +++ b/src/Plugin.InAppBilling/InAppBilling.ios.cs @@ -244,7 +244,7 @@ public async override Task> GetPurchasesAsync( - Task RestoreAsync(CancellationToken cancellationToken) + async Task RestoreAsync(CancellationToken cancellationToken) { var tcsTransaction = new TaskCompletionSource(); @@ -283,7 +283,8 @@ Task RestoreAsync(CancellationToken cancellationToken) // Start receiving restored transactions SKPaymentQueue.DefaultQueue.RestoreCompletedTransactions(); - return tcsTransaction.Task; + var result = await tcsTransaction.Task; + return result; } finally { diff --git a/src/Plugin.InAppBilling/InAppBilling.maccatalyst.cs b/src/Plugin.InAppBilling/InAppBilling.maccatalyst.cs index 98b1201..dc6b1bd 100644 --- a/src/Plugin.InAppBilling/InAppBilling.maccatalyst.cs +++ b/src/Plugin.InAppBilling/InAppBilling.maccatalyst.cs @@ -244,7 +244,7 @@ public async override Task> GetPurchasesAsync( - Task RestoreAsync(CancellationToken cancellationToken) + async Task RestoreAsync(CancellationToken cancellationToken) { var tcsTransaction = new TaskCompletionSource(); @@ -283,7 +283,8 @@ Task RestoreAsync(CancellationToken cancellationToken) // Start receiving restored transactions SKPaymentQueue.DefaultQueue.RestoreCompletedTransactions(); - return tcsTransaction.Task; + var result = await tcsTransaction.Task; + return result; } finally { diff --git a/src/Plugin.InAppBilling/InAppBilling.macos.cs b/src/Plugin.InAppBilling/InAppBilling.macos.cs index 98b1201..dc6b1bd 100644 --- a/src/Plugin.InAppBilling/InAppBilling.macos.cs +++ b/src/Plugin.InAppBilling/InAppBilling.macos.cs @@ -244,7 +244,7 @@ public async override Task> GetPurchasesAsync( - Task RestoreAsync(CancellationToken cancellationToken) + async Task RestoreAsync(CancellationToken cancellationToken) { var tcsTransaction = new TaskCompletionSource(); @@ -283,7 +283,8 @@ Task RestoreAsync(CancellationToken cancellationToken) // Start receiving restored transactions SKPaymentQueue.DefaultQueue.RestoreCompletedTransactions(); - return tcsTransaction.Task; + var result = await tcsTransaction.Task; + return result; } finally { From 38dde50cc18cef458a34958be2e9b41fad582973 Mon Sep 17 00:00:00 2001 From: NGumby Date: Wed, 14 Aug 2024 16:40:55 -0400 Subject: [PATCH 21/22] Update comments --- src/Plugin.InAppBilling/InAppBilling.android.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Plugin.InAppBilling/InAppBilling.android.cs b/src/Plugin.InAppBilling/InAppBilling.android.cs index dda0d80..d6342d7 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -221,6 +221,8 @@ public override async Task> GetPurchasesHistor /// Sku or ID of product that will replace the old one /// Purchase token of original subscription /// Proration mode (1 - ImmediateWithTimeProration, 2 - ImmediateAndChargeProratedPrice, 3 - ImmediateWithoutProration, 4 - Deferred) + /// 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. /// Purchase details public override async Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration, string obfuscatedAccountId = null, string obfuscatedProfileId = null) { From 2b4e30c564c2f12a1f6473091b3175cd6e4b3bee Mon Sep 17 00:00:00 2001 From: Christian Lavallee Date: Fri, 27 Sep 2024 11:54:02 -0400 Subject: [PATCH 22/22] #7649 Convert AppCenter to Sentry ajout de ACTIVATE_STREAMING pour desactiver au besoin ajout pour iOS --- nuget/readme.txt | 32 +------------------ .../Plugin.InAppBilling.csproj | 13 ++++---- 2 files changed, 8 insertions(+), 37 deletions(-) diff --git a/nuget/readme.txt b/nuget/readme.txt index 6d582f8..5ba63f6 100644 --- a/nuget/readme.txt +++ b/nuget/readme.txt @@ -1,31 +1 @@ -In-App Billing Plugin for .NET MAUI - -Version 8.0+ - .NET 8+ -1. Updated APIs and you must target .NET 8 - -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 - - -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 -3.) We are now using Android Billing version 4 -4.) Major breaking chanages across the API including AcknowledgePurchaseAsync being changed to FinalizePurchaseAsync -5.) Tons of new APIs when you get information about products - -Please read documentation for all changes at https://github.com/jamesmontemagno/InAppBillingPlugin - -Version 4.0 has significant updates. - -1.) You must compile and target against Android 10 or higher -2.) On Android you must handle pending transactions and call `FinalizePurchaseAsync` when done -3.) On Android HandleActivityResult has been removed. -4.) We now use Xamarin.Essentials and setup is required per docs. - -Find the latest setup guides, documentation, and testing instructions at: -https://github.com/jamesmontemagno/InAppBillingPlugin +This is until our pull request from compusport/plugin.inappbilling is merged into the main repository. diff --git a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj index 7d6de8d..1d6a28d 100644 --- a/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj +++ b/src/Plugin.InAppBilling/Plugin.InAppBilling.csproj @@ -1,7 +1,8 @@  - net8.0-android;net8.0-ios;net8.0-maccatalyst;net8.0-macos - $(TargetFrameworks);net8.0-windows10.0.19041.0 + net8.0-android;net8.0-ios + + latest true true @@ -45,11 +46,11 @@ 14.2 - 14.0 - 13.0 + 21.0 - 10.0.17763.0 - 10.0.17763.0 +