From 1de535faeddfd38b39e8d8f5f960a56f3303dcca Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Tue, 3 May 2022 16:11:58 -0700 Subject: [PATCH] don't finish transations and minimize API --- docs/PurchaseConsumable.md | 10 +-- docs/PurchaseNonConsumable.md | 14 +-- docs/PurchaseSubscription.md | 15 ++-- nuget/readme.txt | 3 +- src/Plugin.InAppBilling/Converters.android.cs | 6 +- .../InAppBilling.android.cs | 12 +-- src/Plugin.InAppBilling/InAppBilling.apple.cs | 85 +++++-------------- src/Plugin.InAppBilling/InAppBilling.uwp.cs | 10 +-- .../Shared/BaseInAppBilling.shared.cs | 31 ++----- .../Shared/IInAppBilling.shared.cs | 30 ++----- .../Shared/InAppBillingPurchase.shared.cs | 5 ++ 11 files changed, 71 insertions(+), 150 deletions(-) diff --git a/docs/PurchaseConsumable.md b/docs/PurchaseConsumable.md index 16d73b6..b23dc8c 100644 --- a/docs/PurchaseConsumable.md +++ b/docs/PurchaseConsumable.md @@ -11,11 +11,9 @@ All purchases go through the `PurchaseAsync` method and you must always `Connect Consumables are unique and work a bit different on each platform and the `ConsumePurchaseAsync` may need to be called after making the purchase: * Apple: You must consume the purchase (this finishes the transaction), starting in 5.x and 6.x will not auto do this. -* Android: You must consume before purchasing again +* Android: You must consume before purchasing again, it also acts as a way of acknowledging the transation * Microsoft: You must consume before purchasing again -The reason for forcing you to consume is that some platforms will not receive the consumable purchase based when getting them. For this reason, we have introduced `ItemType.InAppPurchaseConsumable` specificaly for iOS. If you pass in `ItemType.InAppPurchase` then it will auto consume the purchase. This is what used to happen in 4.0. - ### Purchase Item ```csharp /// @@ -37,10 +35,10 @@ Task PurchaseAsync(string productId, ItemType itemType, II /// Consume a purchase with a purchase token. /// /// Id or Sku of product -/// Original Purchase Token +/// Original Purchase Token /// If consumed successful /// If an error occurs during processing -Task ConsumePurchaseAsync(string productId, string purchaseToken); +Task ConsumePurchaseAsync(string productId, string transactionIdentifier); ``` @@ -72,7 +70,7 @@ public async Task PurchaseItem(string productId) // here you may want to call your backend or process something in your app. - var wasConsumed = await CrossInAppBilling.Current.ConsumePurchaseAsync(purchase.ProductId, purchase.PurchaseToken); + var wasConsumed = await CrossInAppBilling.Current.ConsumePurchaseAsync(purchase.ProductId, purchase.TransactionIdentifier); if(wasConsumed) { diff --git a/docs/PurchaseNonConsumable.md b/docs/PurchaseNonConsumable.md index 7323e82..d524df6 100644 --- a/docs/PurchaseNonConsumable.md +++ b/docs/PurchaseNonConsumable.md @@ -21,7 +21,9 @@ All purchases go through the `PurchaseAsync` method and you must always `Connect Task PurchaseAsync(string productId, ItemType itemType, IInAppBillingVerifyPurchase verifyPurchase = null); ``` -On Android you must call `AcknowledgePurchaseAsync` 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. +On Android you must call `FinalizeAndAcknowlegeAsync` 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. + +On iOS you must also call Example: ```csharp @@ -46,12 +48,10 @@ public async Task PurchaseItem(string productId) //did not purchase } else if(purchase.State == PurchaseState.Purchased) - { - //purchased! - if(Device.RuntimePlatform == Device.Android) - { - // Must call AcknowledgePurchaseAsync else the purchase will be refunded - } + { + var ack = await CrossInAppBilling.Current.FinalizeAndAcknowlegeAsync(purchase.TransactionIdentifier); + + // Handle if acknowledge was successful or not } } catch (InAppBillingPurchaseException purchaseEx) diff --git a/docs/PurchaseSubscription.md b/docs/PurchaseSubscription.md index 91eb782..6332350 100644 --- a/docs/PurchaseSubscription.md +++ b/docs/PurchaseSubscription.md @@ -23,7 +23,10 @@ All purchases go through the `PurchaseAsync` method and you must always `Connect Task PurchaseAsync(string productId, ItemType itemType, IInAppBillingVerifyPurchase verifyPurchase = null, string obfuscatedAccountId = null, string obfuscatedProfileId = null); ``` -On Android you must call `AcknowledgePurchaseAsync` 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. +On Android you must call `FinalizeAndAcknowlegeAsync` 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. + + +You must also call this on iOS to finalize and acknowlege the transation. Example: ```csharp @@ -47,13 +50,11 @@ public async Task PurchaseItem(string productId, string payload) { //did not purchase } - else + else if(purchase.State == PurchaseState.Purchased) { - //purchased! - if(Device.RuntimePlatform == Device.Android) - { - // Must call AcknowledgePurchaseAsync else the purchase will be refunded - } + var ack = await CrossInAppBilling.Current.FinalizeAndAcknowlegeAsync(purchase.TransactionIdentifier); + + // Handle if acknowledge was successful or not } } catch (InAppBillingPurchaseException purchaseEx) diff --git a/nuget/readme.txt b/nuget/readme.txt index 0bac19f..3685cc5 100644 --- a/nuget/readme.txt +++ b/nuget/readme.txt @@ -4,11 +4,12 @@ Version 5.0+ has more 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.) SUPER IMPORTANT: iOS has changed the way in which in-app purchases are handled. They are no longer automatically finished and you must call `FinalizeAndAcknowlegeAsync(string transactionIdentifier)` on each transaction! 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 AcknowledgePurchaseAsync` when done +2.) On Android you must handle pending transactions and call `FinalizeAndAcknowlegeAsync` when done 3.) On Android HandleActivityResult has been removed. 4.) We now use Xamarin.Essentials and setup is required per docs. diff --git a/src/Plugin.InAppBilling/Converters.android.cs b/src/Plugin.InAppBilling/Converters.android.cs index 9416f81..134ec7d 100644 --- a/src/Plugin.InAppBilling/Converters.android.cs +++ b/src/Plugin.InAppBilling/Converters.android.cs @@ -23,7 +23,8 @@ internal static InAppBillingPurchase ToIABPurchase(this Purchase purchase) PurchaseToken = purchase.PurchaseToken, TransactionDateUtc = DateTimeOffset.FromUnixTimeMilliseconds(purchase.PurchaseTime).DateTime, ObfuscatedAccountId = purchase.AccountIdentifiers?.ObfuscatedAccountId, - ObfuscatedProfileId = purchase.AccountIdentifiers?.ObfuscatedProfileId + ObfuscatedProfileId = purchase.AccountIdentifiers?.ObfuscatedProfileId, + TransactionIdentifier = purchase.PurchaseToken }; finalPurchase.State = purchase.PurchaseState switch @@ -48,7 +49,8 @@ internal static InAppBillingPurchase ToIABPurchase(this PurchaseHistoryRecord pu ProductIds = purchase.Skus, PurchaseToken = purchase.PurchaseToken, TransactionDateUtc = DateTimeOffset.FromUnixTimeMilliseconds(purchase.PurchaseTime).DateTime, - State = PurchaseState.Unknown + State = PurchaseState.Unknown, + TransactionIdentifier = purchase.PurchaseToken }; } diff --git a/src/Plugin.InAppBilling/InAppBilling.android.cs b/src/Plugin.InAppBilling/InAppBilling.android.cs index 6af7405..c6b5f5e 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -170,7 +170,7 @@ public async override Task> GetProductInfoAsync } - public override Task> GetPurchasesAsync(ItemType itemType, List doNotFinishTransactionIds = null) + public override Task> GetPurchasesAsync(ItemType itemType) { if (BillingClient == null) throw new InAppBillingPurchaseException(PurchaseError.ServiceUnavailable, "You are not connected to the Google Play App store."); @@ -381,13 +381,13 @@ async Task PurchaseAsync(string productSku, string itemTyp } - public async override Task AcknowledgePurchaseAsync(string purchaseToken) + public async override Task FinalizeAndAcknowlegeAsync(string transactionIdentifier) { if (BillingClient == null || !IsConnected) throw new InAppBillingPurchaseException(PurchaseError.ServiceUnavailable, "You are not connected to the Google Play App store."); var acknowledgeParams = AcknowledgePurchaseParams.NewBuilder() - .SetPurchaseToken(purchaseToken).Build(); + .SetPurchaseToken(transactionIdentifier).Build(); var result = await BillingClient.AcknowledgePurchaseAsync(acknowledgeParams); @@ -400,9 +400,9 @@ public async override Task AcknowledgePurchaseAsync(string purchaseToken) /// Consume a purchase with a purchase token. /// /// Id or Sku of product - /// Original Purchase Token + /// Original Purchase Token /// If consumed successful - public override async Task ConsumePurchaseAsync(string productId, string purchaseToken, string purchaseId, List doNotFinishProductIds = null) + public override async Task ConsumePurchaseAsync(string productId, string transactionIdentifier) { if (BillingClient == null || !IsConnected) { @@ -411,7 +411,7 @@ public override async Task ConsumePurchaseAsync(string productId, string p var consumeParams = ConsumeParams.NewBuilder() - .SetPurchaseToken(purchaseToken) + .SetPurchaseToken(transactionIdentifier) .Build(); var result = await BillingClient.ConsumeAsync(consumeParams); diff --git a/src/Plugin.InAppBilling/InAppBilling.apple.cs b/src/Plugin.InAppBilling/InAppBilling.apple.cs index c446700..6f3d578 100644 --- a/src/Plugin.InAppBilling/InAppBilling.apple.cs +++ b/src/Plugin.InAppBilling/InAppBilling.apple.cs @@ -217,10 +217,10 @@ Task> GetProductAsync(string[] productId) /// /// /// - public async override Task> GetPurchasesAsync(ItemType itemType, List doNotFinishTransactionIds = null) + public async override Task> GetPurchasesAsync(ItemType itemType) { Init(); - var purchases = await RestoreAsync(doNotFinishTransactionIds); + var purchases = await RestoreAsync(); var comparer = new InAppBillingPurchaseComparer(); return purchases @@ -231,7 +231,7 @@ public async override Task> GetPurchasesAsync( - Task RestoreAsync(List doNotFinishTransactionIds) + Task RestoreAsync() { var tcsTransaction = new TaskCompletionSource(); @@ -240,7 +240,6 @@ Task RestoreAsync(List doNotFinishTransactionIds Action handler = null; handler = new Action(transactions => { - paymentObserver.DoNotFinishTransactionIds = new List(); // Unsubscribe from future events paymentObserver.TransactionsRestored -= handler; @@ -259,7 +258,6 @@ Task RestoreAsync(List doNotFinishTransactionIds }); - paymentObserver.DoNotFinishTransactionIds = doNotFinishTransactionIds; paymentObserver.TransactionsRestored += handler; foreach (var trans in SKPaymentQueue.DefaultQueue.Transactions) @@ -318,7 +316,8 @@ public async override Task PurchaseAsync(string productId, { TransactionDateUtc = reference.AddSeconds(p.TransactionDate.SecondsSinceReferenceDate), Id = p.TransactionIdentifier, - ProductId = p.Payment?.ProductIdentifier ?? string.Empty, + TransactionIdentifier = p.TransactionIdentifier, + ProductId = p.Payment?.ProductIdentifier ?? string.Empty, ProductIds = new string[] { p.Payment?.ProductIdentifier ?? string.Empty }, State = p.GetPurchaseState(), #if __IOS__ || __TVOS__ @@ -344,7 +343,6 @@ async Task PurchaseAsync(string productId, ItemType itemTy if (productId != tran.Payment.ProductIdentifier) return; - paymentObserver.DoNotFinishTransactionIds = new List(); // Unsubscribe from future events paymentObserver.TransactionCompleted -= handler; @@ -390,11 +388,6 @@ async Task PurchaseAsync(string productId, ItemType itemTy tcsTransaction.TrySetException(new InAppBillingPurchaseException(error, description)); }); - - if (itemType == ItemType.InAppPurchaseConsumable) - paymentObserver.DoNotFinishTransactionIds = new List(new[] { productId }); - else - paymentObserver.DoNotFinishTransactionIds = new List(); paymentObserver.TransactionCompleted += handler; @@ -441,39 +434,29 @@ public override string ReceiptData /// Consume a purchase with a purchase token. /// /// Id or Sku of product - /// Original Purchase Token - /// Original transaction id + /// Original Purchase Token /// If consumed successful /// If an error occurs during processing - public override Task ConsumePurchaseAsync(string productId, string purchaseToken, string purchaseId, List doNotFinishProductIds = null) => - FinishTransaction(purchaseId, doNotFinishProductIds); - - - /// - /// Manually finish a transaction - /// - /// - /// - public override Task FinishTransaction(InAppBillingPurchase purchase, List doNotFinishProductIds = null) => - FinishTransaction(purchase?.Id, doNotFinishProductIds); + public override Task ConsumePurchaseAsync(string productId, string transactionIdentifier) => + FinalizeAndAcknowlegeAsync(transactionIdentifier); /// /// Finish a transaction manually /// - /// + /// /// /// - public override async Task FinishTransaction(string purchaseId, List doNotFinishProductIds = null) - { - if (string.IsNullOrWhiteSpace(purchaseId)) - throw new ArgumentException("Purchase Token must be valid", nameof(purchaseId)); - - var purchases = await RestoreAsync(doNotFinishProductIds); + public async override Task FinalizeAndAcknowlegeAsync(string transactionIdentifier) + { + if (string.IsNullOrWhiteSpace(transactionIdentifier)) + throw new ArgumentException("Purchase Token must be valid", nameof(transactionIdentifier)); + + var purchases = await RestoreAsync(); if (purchases == null) return false; - var transaction = purchases.Where(p => p.TransactionIdentifier == purchaseId).FirstOrDefault(); + var transaction = purchases.Where(p => p.TransactionIdentifier == transactionIdentifier).FirstOrDefault(); if (transaction == null) return false; @@ -569,8 +552,6 @@ class PaymentObserver : SKPaymentTransactionObserver public event Action TransactionCompleted; public event Action TransactionsRestored; - public List DoNotFinishTransactionIds { get; set; } - readonly List restoredTransactions = new (); readonly Action onPurchaseSuccess; readonly Func onShouldAddStorePayment; @@ -608,12 +589,9 @@ public override void UpdatedTransactions(SKPaymentQueue queue, SKPaymentTransact TransactionCompleted?.Invoke(transaction, true); onPurchaseSuccess?.Invoke(transaction.ToIABPurchase()); - - Finish(transaction); break; case SKPaymentTransactionState.Failed: TransactionCompleted?.Invoke(transaction, false); - Finish(transaction); break; default: break; @@ -621,25 +599,6 @@ public override void UpdatedTransactions(SKPaymentQueue queue, SKPaymentTransact } } - void Finish(SKPaymentTransaction transaction) - { - - //checks to see if we should or shouldn't finish this. - var id = transaction.Payment?.ProductIdentifier ?? string.Empty; - var containsId = DoNotFinishTransactionIds?.Contains(id) ?? false; - if (containsId) - return; - - try - { - SKPaymentQueue.DefaultQueue.FinishTransaction(transaction); - } - catch(Exception ex) - { - Debug.WriteLine("Couldn't finish transaction: " + ex); - } - } - public override void RestoreCompletedTransactionsFinished(SKPaymentQueue queue) { if (restoredTransactions == null) @@ -652,14 +611,7 @@ public override void RestoreCompletedTransactionsFinished(SKPaymentQueue queue) // Clear out the list of incoming restore transactions for future requests restoredTransactions.Clear(); - TransactionsRestored?.Invoke(allTransactions); - - - foreach (var transaction in allTransactions) - { - Finish(transaction); - } - + TransactionsRestored?.Invoke(allTransactions); } // Failure, just fire with null @@ -696,7 +648,8 @@ public static InAppBillingPurchase ToIABPurchase(this SKPaymentTransaction trans { TransactionDateUtc = NSDateToDateTimeUtc(transaction.TransactionDate), Id = p.TransactionIdentifier, - ProductId = p.Payment?.ProductIdentifier ?? string.Empty, + TransactionIdentifier = p.TransactionIdentifier, + ProductId = p.Payment?.ProductIdentifier ?? string.Empty, ProductIds = new string[] { p.Payment?.ProductIdentifier ?? string.Empty }, State = p.GetPurchaseState(), PurchaseToken = finalToken diff --git a/src/Plugin.InAppBilling/InAppBilling.uwp.cs b/src/Plugin.InAppBilling/InAppBilling.uwp.cs index 423d169..d44992f 100644 --- a/src/Plugin.InAppBilling/InAppBilling.uwp.cs +++ b/src/Plugin.InAppBilling/InAppBilling.uwp.cs @@ -75,9 +75,8 @@ public async override Task> GetProductInfoAsync /// Get all pruchases /// /// - /// /// - public async override Task> GetPurchasesAsync(ItemType itemType, List doNotFinishTransactionIds = null) + public async override Task> GetPurchasesAsync(ItemType itemType) { // Get list of product receipts from store or simulator var xmlReceipt = await CurrentAppMock.GetAppReceiptAsync(InTestingMode); @@ -123,12 +122,12 @@ public async override Task PurchaseAsync(string productId, /// Consume a purchase with a purchase token. /// /// Id or Sku of product - /// Original Purchase Token + /// Original Purchase Token /// If consumed successful /// If an error occures during processing - public async override Task ConsumePurchaseAsync(string productId, string purchaseToken, string purchaseId, List doNotFinishProductIds = null) + public async override Task ConsumePurchaseAsync(string productId, string transactionIdentifier) { - var result = await CurrentAppMock.ReportConsumableFulfillmentAsync(InTestingMode, productId, new Guid(purchaseToken)); + var result = await CurrentAppMock.ReportConsumableFulfillmentAsync(InTestingMode, productId, new Guid(transactionIdentifier)); return result switch { FulfillmentResult.ServerError => throw new InAppBillingPurchaseException(PurchaseError.AppStoreUnavailable), @@ -209,6 +208,7 @@ public static IEnumerable ToInAppBillingPurchase(this stri AutoRenewing = false // Not supported by UWP yet }; purchase.PurchaseToken = purchase.Id; + purchase.TransactionIdentifier = purchase.Id; purchase.ProductIds = new string[] { purchase.ProductId }; // Map native UWP status to PurchaseState diff --git a/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs b/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs index 15784d0..844eac6 100644 --- a/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs +++ b/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs @@ -62,9 +62,8 @@ public abstract class BaseInAppBilling : IInAppBilling, IDisposable /// Get all current purchases for a specific product type. If verification fails for some purchase, it's not contained in the result. /// /// Type of product - /// List of ids not to finish (iOS only) /// The current purchases - public abstract Task> GetPurchasesAsync(ItemType itemType, List doNotFinishProductIds = null); + public abstract Task> GetPurchasesAsync(ItemType itemType); @@ -99,12 +98,11 @@ public abstract class BaseInAppBilling : IInAppBilling, IDisposable /// /// Consume a purchase with a purchase token. /// - /// Original Purchase Token - /// Original transaction id - /// List of ids not to finish (iOS only) + /// Product Id + /// Original Purchase Token /// If consumed successful /// If an error occurs during processing - public abstract Task ConsumePurchaseAsync(string productId, string purchaseToken, string purchaseId, List doNotFinishProductIds = null); + public abstract Task ConsumePurchaseAsync(string productId, string transactionIdentifier); /// /// Dispose of class and parent classes @@ -140,29 +138,12 @@ public virtual void Dispose(bool disposing) disposed = true; } } - - /// - /// Manually finish a transaction - /// - /// - /// List of ids not to finish (iOS only) - /// - public virtual Task FinishTransaction(InAppBillingPurchase purchase, List doNotFinishProductIds = null) => Task.FromResult(true); - - /// - /// manually finish a transaction - /// - /// Original transaction id - /// List of ids not to finish (iOS only) - /// - public virtual Task FinishTransaction(string purchaseId, List doNotFinishProductIds = null) => Task.FromResult(true); - /// /// acknowledge a purchase /// - /// + /// /// - public virtual Task AcknowledgePurchaseAsync(string purchaseToken) => Task.FromResult(true); + public virtual Task FinalizeAndAcknowlegeAsync(string transactionIdentifier) => Task.FromResult(true); /// /// iOS: Displays a sheet that enables users to redeem subscription offer codes that you configure in App Store Connect. diff --git a/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs b/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs index 6a0bf83..f957825 100644 --- a/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs +++ b/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs @@ -29,9 +29,9 @@ public interface IInAppBilling : IDisposable /// /// Manually acknowledge a purchase /// - /// + /// /// - Task AcknowledgePurchaseAsync(string purchaseToken); + Task FinalizeAndAcknowlegeAsync(string transactionIdentifier); /// /// Connect to billing service @@ -57,9 +57,8 @@ public interface IInAppBilling : IDisposable /// 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 - /// All of Ids of products that you do not want to auto finish (iOS consumables) /// The current purchases - Task> GetPurchasesAsync(ItemType itemType, List doNotFinishProductIds = null); + Task> GetPurchasesAsync(ItemType itemType); /// @@ -94,35 +93,16 @@ public interface IInAppBilling : IDisposable /// Consume a purchase with a purchase token. /// /// Product id or sku - /// Original Purchase Token - /// Original Transaction Id - /// All of Ids of products that you do not want to auto finish (iOS consumables) + /// Original Purchase Token /// If consumed successful /// If an error occurs during processing - Task ConsumePurchaseAsync(string productId, string purchaseToken, string purchaseId, List doNotFinishProductIds = null); - - /// - /// Manually finish a transaction - /// - /// - /// All of Ids of products that you do not want to auto finish (iOS consumables) - /// - Task FinishTransaction(InAppBillingPurchase purchase, List doNotFinishProductIds = null); - - /// - /// Manually finish a transaction - /// - /// Original transaction id - /// All of Ids of products that you do not want to auto finish (iOS consumables) - /// - Task FinishTransaction(string purchaseId, List doNotFinishProductIds = null); + Task ConsumePurchaseAsync(string productId, string transactionIdentifier); /// /// Get receipt data on iOS /// string ReceiptData { get; } - /// /// Gets if user is allowed to make a payment. /// diff --git a/src/Plugin.InAppBilling/Shared/InAppBillingPurchase.shared.cs b/src/Plugin.InAppBilling/Shared/InAppBillingPurchase.shared.cs index 3a199ee..9dab02a 100644 --- a/src/Plugin.InAppBilling/Shared/InAppBillingPurchase.shared.cs +++ b/src/Plugin.InAppBilling/Shared/InAppBillingPurchase.shared.cs @@ -31,6 +31,11 @@ public InAppBillingPurchase() /// public string Id { get; set; } + /// + /// TransactionIdentifier - This is the Id/Token that needs to be acknowledge/finalized + /// + public string TransactionIdentifier { get; set; } + /// /// Transaction date in UTC ///