Skip to content

Commit

Permalink
subscription downgrade / upgrade (Google Play Billing Library 4.0.0 c…
Browse files Browse the repository at this point in the history
…ompatible)
  • Loading branch information
RadoslawKubas committed Aug 15, 2021
1 parent 17af6e5 commit 409f4f2
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 0 deletions.
93 changes: 93 additions & 0 deletions src/Plugin.InAppBilling/InAppBilling.android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,99 @@ public override Task<IEnumerable<InAppBillingPurchase>> GetPurchasesAsync(ItemTy
return Task.FromResult(purchasesResult.PurchasesList.Select(p => p.ToIABPurchase()));
}

/// <summary>
/// (Android specific) Upgrade/Downgrade/Change a previously purchased subscription
/// </summary>
/// <param name="newProductId">Sku or ID of product that will replace the old one</param>
/// <param name="oldProductId">Sku or ID of product that needs to be upgraded</param>
/// <param name="purchaseTokenOfOriginalSubscription">Purchase token of original subscription</param>
/// <param name="prorationMode">Proration mode (1 - ImmediateWithTimeProration, 2 - ImmediateAndChargeProratedPrice, 3 - ImmediateWithoutProration, 4 - Deferred)</param>
/// <param name="verifyPurchase">Verify Purchase implementation</param>
/// <returns>Purchase details</returns>
public override async Task<InAppBillingPurchase> UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, int prorationMode = 1, IInAppBillingVerifyPurchase verifyPurchase = null)
{
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;
}

var purchase = await UpgradePurchasedSubscriptionInternalAsync(newProductId, purchaseTokenOfOriginalSubscription, prorationMode, verifyPurchase);

return purchase;
}

async Task<InAppBillingPurchase> UpgradePurchasedSubscriptionInternalAsync(string newProductId, string purchaseTokenOfOriginalSubscription, int prorationMode = 1, IInAppBillingVerifyPurchase verifyPurchase = null)
{
var itemType = BillingClient.SkuType.Subs;

if (tcsPurchase?.Task != null && !tcsPurchase.Task.IsCompleted)
{
return null;
}

var skuDetailsParams = SkuDetailsParams.NewBuilder()
.SetType(itemType)
.SetSkusList(new List<string> { newProductId })
.Build();

var skuDetailsResult = await BillingClient.QuerySkuDetailsAsync(skuDetailsParams);
ParseBillingResult(skuDetailsResult?.Result);

var skuDetails = skuDetailsResult?.SkuDetails.FirstOrDefault();

if (skuDetails == null)
throw new ArgumentException($"{newProductId} does not exist");

//1 - BillingFlowParams.ProrationMode.ImmediateWithTimeProration
//2 - BillingFlowParams.ProrationMode.ImmediateAndChargeProratedPrice
//3 - BillingFlowParams.ProrationMode.ImmediateWithoutProration
//4 - BillingFlowParams.ProrationMode.Deferred

var updateParams = BillingFlowParams.SubscriptionUpdateParams.NewBuilder()
.SetOldSkuPurchaseToken(purchaseTokenOfOriginalSubscription)
.SetReplaceSkusProrationMode(prorationMode)
.Build();

var flowParams = BillingFlowParams.NewBuilder()
.SetSkuDetails(skuDetails)
.SetSubscriptionUpdateParams(updateParams)
.Build();

tcsPurchase = new TaskCompletionSource<(BillingResult billingResult, IList<Android.BillingClient.Api.Purchase> purchases)>();
var responseCode = BillingClient.LaunchBillingFlow(Activity, flowParams);

ParseBillingResult(responseCode);

var result = await tcsPurchase.Task;
ParseBillingResult(result.billingResult);

//we are only buying 1 thing.
var androidPurchase = result.purchases?.FirstOrDefault(p => p.Skus.Contains(newProductId));

//for some reason the data didn't come back
if (androidPurchase == null)
{
var purchases = await GetPurchasesAsync(itemType == BillingClient.SkuType.Inapp ? ItemType.InAppPurchase : ItemType.Subscription);
return purchases.FirstOrDefault(p => p.ProductId == newProductId);
}

var data = androidPurchase.OriginalJson;
var signature = androidPurchase.Signature;

var purchase = androidPurchase.ToIABPurchase();
if (verifyPurchase == null || await verifyPurchase.VerifyPurchase(data, signature, newProductId, purchase.Id))
return purchase;

return null;
}

/// <summary>
/// Purchase a specific product or subscription
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions src/Plugin.InAppBilling/InAppBilling.apple.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,16 @@ public async override Task<InAppBillingPurchase> PurchaseAsync(string productId,
return purchase;
}

/// <summary>
/// (iOS not supported) Apple store manages upgrades natively when subscriptions of the same group are purchased.
/// </summary>
/// <exception cref="NotImplementedException">iOS not supported</exception>
public override Task<InAppBillingPurchase> UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, int prorationMode = 1, IInAppBillingVerifyPurchase verifyPurchase = null)
{
throw new NotImplementedException("iOS not supported. Apple store manages upgrades natively when subscriptions of the same group are purchased.");
}


public override string ReceiptData
{
get
Expand Down
9 changes: 9 additions & 0 deletions src/Plugin.InAppBilling/InAppBilling.uwp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ public async override Task<InAppBillingPurchase> PurchaseAsync(string productId,

}

/// <summary>
/// (UWP not supported) Upgrade/Downgrade/Change a previously purchased subscription
/// </summary>
/// <exception cref="NotImplementedException">UWP not supported</exception>
public override Task<InAppBillingPurchase> UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, int prorationMode = 1, IInAppBillingVerifyPurchase verifyPurchase = null)
{
throw new NotImplementedException("UWP not supported.");
}

/// <summary>
/// Consume a purchase with a purchase token.
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ public abstract class BaseInAppBilling : IInAppBilling, IDisposable
/// <exception cref="InAppBillingPurchaseException">If an error occures during processing</exception>
public abstract Task<InAppBillingPurchase> PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null);

/// <summary>
/// (Android specific) Upgrade/Downgrade a previously purchased subscription
/// </summary>
/// <param name="newProductId">Sku or ID of product that will replace the old one</param>
/// <param name="purchaseTokenOfOriginalSubscription">Purchase token of original subscription (can not be null)</param>
/// <param name="prorationMode">Proration mode</param>
/// <param name="verifyPurchase">Verify Purchase implementation</param>
/// <returns>Purchase details</returns>
public abstract Task<InAppBillingPurchase> UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, int prorationMode = 1, IInAppBillingVerifyPurchase verifyPurchase = null);

/// <summary>
/// Consume a purchase with a purchase token.
/// </summary>
Expand Down
11 changes: 11 additions & 0 deletions src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ public interface IInAppBilling : IDisposable
/// <exception cref="InAppBillingPurchaseException">If an error occures during processing</exception>
Task<InAppBillingPurchase> PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null);

/// <summary>
/// (Android specific) Upgrade/Downgrade a previously purchased subscription
/// </summary>
/// <param name="newProductId">Sku or ID of product that will replace the old one</param>
/// <param name="purchaseTokenOfOriginalSubscription">Purchase token of original subscription (can not be null)</param>
/// <param name="prorationMode">Proration mode (1 - ImmediateWithTimeProration, 2 - ImmediateAndChargeProratedPrice, 3 - ImmediateWithoutProration, 4 - Deferred)</param>
/// <param name="verifyPurchase">Verify Purchase implementation</param>
/// <returns>Purchase details</returns>
/// <exception cref="InAppBillingPurchaseException">If an error occures during processing</exception>
Task<InAppBillingPurchase> UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, int prorationMode = 1, IInAppBillingVerifyPurchase verifyPurchase = null);

/// <summary>
/// Consume a purchase with a purchase token.
/// </summary>
Expand Down

0 comments on commit 409f4f2

Please sign in to comment.