Skip to content

Commit

Permalink
Merge pull request #468 from jamesmontemagno/no-finish
Browse files Browse the repository at this point in the history
don't finish transactions and minimize API
  • Loading branch information
jamesmontemagno authored May 11, 2022
2 parents 55b1441 + 8634946 commit 9829427
Show file tree
Hide file tree
Showing 11 changed files with 72 additions and 150 deletions.
10 changes: 4 additions & 6 deletions docs/PurchaseConsumable.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 transaction
* 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
/// <summary>
Expand All @@ -37,10 +35,10 @@ Task<InAppBillingPurchase> PurchaseAsync(string productId, ItemType itemType, II
/// Consume a purchase with a purchase token.
/// </summary>
/// <param name="productId">Id or Sku of product</param>
/// <param name="purchaseToken">Original Purchase Token</param>
/// <param name="transactionIdentifier">Original Purchase Token</param>
/// <returns>If consumed successful</returns>
/// <exception cref="InAppBillingPurchaseException">If an error occurs during processing</exception>
Task<InAppBillingPurchase> ConsumePurchaseAsync(string productId, string purchaseToken);
Task<InAppBillingPurchase> ConsumePurchaseAsync(string productId, string transactionIdentifier);
```


Expand Down Expand Up @@ -72,7 +70,7 @@ public async Task<bool> 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)
{
Expand Down
14 changes: 7 additions & 7 deletions docs/PurchaseNonConsumable.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ All purchases go through the `PurchaseAsync` method and you must always `Connect
Task<InAppBillingPurchase> 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 `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.

On iOS you must also call

Example:
```csharp
Expand All @@ -46,12 +48,10 @@ public async Task<bool> 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.FinalizePurchaseAsync(purchase.TransactionIdentifier);

// Handle if acknowledge was successful or not
}
}
catch (InAppBillingPurchaseException purchaseEx)
Expand Down
15 changes: 8 additions & 7 deletions docs/PurchaseSubscription.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ All purchases go through the `PurchaseAsync` method and you must always `Connect
Task<InAppBillingPurchase> 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 `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.


You must also call this on iOS to finalize and acknowlege the transaction.

Example:
```csharp
Expand All @@ -47,13 +50,11 @@ public async Task<bool> 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.FinalizePurchaseAsync(purchase.TransactionIdentifier);

// Handle if acknowledge was successful or not
}
}
catch (InAppBillingPurchaseException purchaseEx)
Expand Down
4 changes: 3 additions & 1 deletion nuget/readme.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
In App Billing Plugin for .NET MAUI, Xamarin, & Windows

SUPER IMPORTANT: iOS has changed the way in which in-app purchases are handled. They are no longer automatically finished and you must call `FinalizePurchaseAsync(string transactionIdentifier)` on each transaction!

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
Expand All @@ -8,7 +10,7 @@ Version 5.0+ has more significant updates!
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 `FinalizePurchaseAsync` when done
3.) On Android HandleActivityResult has been removed.
4.) We now use Xamarin.Essentials and setup is required per docs.

Expand Down
6 changes: 4 additions & 2 deletions src/Plugin.InAppBilling/Converters.android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
};
}

Expand Down
12 changes: 6 additions & 6 deletions src/Plugin.InAppBilling/InAppBilling.android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ public async override Task<IEnumerable<InAppBillingProduct>> GetProductInfoAsync
}


public override Task<IEnumerable<InAppBillingPurchase>> GetPurchasesAsync(ItemType itemType, List<string> doNotFinishTransactionIds = null)
public override Task<IEnumerable<InAppBillingPurchase>> GetPurchasesAsync(ItemType itemType)
{
if (BillingClient == null)
throw new InAppBillingPurchaseException(PurchaseError.ServiceUnavailable, "You are not connected to the Google Play App store.");
Expand Down Expand Up @@ -381,13 +381,13 @@ async Task<InAppBillingPurchase> PurchaseAsync(string productSku, string itemTyp
}


public async override Task<bool> AcknowledgePurchaseAsync(string purchaseToken)
public async override Task<bool> FinalizePurchaseAsync(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);

Expand All @@ -400,9 +400,9 @@ public async override Task<bool> AcknowledgePurchaseAsync(string purchaseToken)
/// Consume a purchase with a purchase token.
/// </summary>
/// <param name="productId">Id or Sku of product</param>
/// <param name="purchaseToken">Original Purchase Token</param>
/// <param name="transactionIdentifier">Original Purchase Token</param>
/// <returns>If consumed successful</returns>
public override async Task<bool> ConsumePurchaseAsync(string productId, string purchaseToken, string purchaseId, List<string> doNotFinishProductIds = null)
public override async Task<bool> ConsumePurchaseAsync(string productId, string transactionIdentifier)
{
if (BillingClient == null || !IsConnected)
{
Expand All @@ -411,7 +411,7 @@ public override async Task<bool> ConsumePurchaseAsync(string productId, string p


var consumeParams = ConsumeParams.NewBuilder()
.SetPurchaseToken(purchaseToken)
.SetPurchaseToken(transactionIdentifier)
.Build();

var result = await BillingClient.ConsumeAsync(consumeParams);
Expand Down
85 changes: 19 additions & 66 deletions src/Plugin.InAppBilling/InAppBilling.apple.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,10 @@ Task<IEnumerable<SKProduct>> GetProductAsync(string[] productId)
/// </summary>
/// <param name="itemType"></param>
/// <returns></returns>
public async override Task<IEnumerable<InAppBillingPurchase>> GetPurchasesAsync(ItemType itemType, List<string> doNotFinishTransactionIds = null)
public async override Task<IEnumerable<InAppBillingPurchase>> GetPurchasesAsync(ItemType itemType)
{
Init();
var purchases = await RestoreAsync(doNotFinishTransactionIds);
var purchases = await RestoreAsync();

var comparer = new InAppBillingPurchaseComparer();
return purchases
Expand All @@ -231,7 +231,7 @@ public async override Task<IEnumerable<InAppBillingPurchase>> GetPurchasesAsync(



Task<SKPaymentTransaction[]> RestoreAsync(List<string> doNotFinishTransactionIds)
Task<SKPaymentTransaction[]> RestoreAsync()
{
var tcsTransaction = new TaskCompletionSource<SKPaymentTransaction[]>();

Expand All @@ -240,7 +240,6 @@ Task<SKPaymentTransaction[]> RestoreAsync(List<string> doNotFinishTransactionIds
Action<SKPaymentTransaction[]> handler = null;
handler = new Action<SKPaymentTransaction[]>(transactions =>
{
paymentObserver.DoNotFinishTransactionIds = new List<string>();
// Unsubscribe from future events
paymentObserver.TransactionsRestored -= handler;
Expand All @@ -259,7 +258,6 @@ Task<SKPaymentTransaction[]> RestoreAsync(List<string> doNotFinishTransactionIds
});


paymentObserver.DoNotFinishTransactionIds = doNotFinishTransactionIds;
paymentObserver.TransactionsRestored += handler;

foreach (var trans in SKPaymentQueue.DefaultQueue.Transactions)
Expand Down Expand Up @@ -318,7 +316,8 @@ public async override Task<InAppBillingPurchase> 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__
Expand All @@ -344,7 +343,6 @@ async Task<SKPaymentTransaction> PurchaseAsync(string productId, ItemType itemTy
if (productId != tran.Payment.ProductIdentifier)
return;
paymentObserver.DoNotFinishTransactionIds = new List<string>();
// Unsubscribe from future events
paymentObserver.TransactionCompleted -= handler;
Expand Down Expand Up @@ -390,11 +388,6 @@ async Task<SKPaymentTransaction> PurchaseAsync(string productId, ItemType itemTy
tcsTransaction.TrySetException(new InAppBillingPurchaseException(error, description));
});

if (itemType == ItemType.InAppPurchaseConsumable)
paymentObserver.DoNotFinishTransactionIds = new List<string>(new[] { productId });
else
paymentObserver.DoNotFinishTransactionIds = new List<string>();

paymentObserver.TransactionCompleted += handler;

Expand Down Expand Up @@ -441,39 +434,29 @@ public override string ReceiptData
/// Consume a purchase with a purchase token.
/// </summary>
/// <param name="productId">Id or Sku of product</param>
/// <param name="purchaseToken">Original Purchase Token</param>
/// <param name="purchaseId">Original transaction id</param>
/// <param name="transactionIdentifier">Original Purchase Token</param>
/// <returns>If consumed successful</returns>
/// <exception cref="InAppBillingPurchaseException">If an error occurs during processing</exception>
public override Task<bool> ConsumePurchaseAsync(string productId, string purchaseToken, string purchaseId, List<string> doNotFinishProductIds = null) =>
FinishTransaction(purchaseId, doNotFinishProductIds);


/// <summary>
/// Manually finish a transaction
/// </summary>
/// <param name="purchase"></param>
/// <returns></returns>
public override Task<bool> FinishTransaction(InAppBillingPurchase purchase, List<string> doNotFinishProductIds = null) =>
FinishTransaction(purchase?.Id, doNotFinishProductIds);
public override Task<bool> ConsumePurchaseAsync(string productId, string transactionIdentifier) =>
FinalizePurchaseAsync(transactionIdentifier);

/// <summary>
/// Finish a transaction manually
/// </summary>
/// <param name="purchaseId"></param>
/// <param name="transactionIdentifier"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public override async Task<bool> FinishTransaction(string purchaseId, List<string> 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<bool> FinalizePurchaseAsync(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;

Expand Down Expand Up @@ -569,8 +552,6 @@ class PaymentObserver : SKPaymentTransactionObserver
public event Action<SKPaymentTransaction, bool> TransactionCompleted;
public event Action<SKPaymentTransaction[]> TransactionsRestored;

public List<string> DoNotFinishTransactionIds { get; set; }

readonly List<SKPaymentTransaction> restoredTransactions = new ();
readonly Action<InAppBillingPurchase> onPurchaseSuccess;
readonly Func<SKPaymentQueue, SKPayment, SKProduct, bool> onShouldAddStorePayment;
Expand Down Expand Up @@ -608,38 +589,16 @@ 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;
}
}
}

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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 9829427

Please sign in to comment.