Skip to content

Commit

Permalink
Merge pull request #404 from RadoslawKubas/android_subscription_update
Browse files Browse the repository at this point in the history
Android subscriptions downgrade / upgrade (Google Play Billing Library 4.0.0 c…
  • Loading branch information
jamesmontemagno authored Sep 8, 2021
2 parents 17af6e5 + 002655c commit b2edd3e
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 0 deletions.
85 changes: 85 additions & 0 deletions src/Plugin.InAppBilling/InAppBilling.android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,91 @@ 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>
/// <returns>Purchase details</returns>
public override async Task<InAppBillingPurchase> UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription,SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration)
{
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);

return purchase;
}

async Task<InAppBillingPurchase> UpgradePurchasedSubscriptionInternalAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode)
{
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((int)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);
}

return androidPurchase.ToIABPurchase();
}

/// <summary>
/// Purchase a specific product or subscription
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions src/Plugin.InAppBilling/InAppBilling.apple.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,14 @@ 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, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration) =>
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
7 changes: 7 additions & 0 deletions src/Plugin.InAppBilling/InAppBilling.uwp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ 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, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration) =>
throw new NotImplementedException("UWP not supported.");

/// <summary>
/// Consume a purchase with a purchase token.
/// </summary>
Expand Down
9 changes: 9 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,15 @@ 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>
/// <returns>Purchase details</returns>
public abstract Task<InAppBillingPurchase> UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration);

/// <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, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration);

/// <summary>
/// Consume a purchase with a purchase token.
/// </summary>
Expand Down
11 changes: 11 additions & 0 deletions src/Plugin.InAppBilling/Shared/ItemType.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,15 @@ public enum ItemType
/// </summary>
Subscription
}

/// <summary>
/// Subcription proration mode
/// </summary>
public enum SubscriptionProrationMode
{
ImmediateWithTimeProration = 1,
ImmediateAndChargeProratedPrice = 2,
ImmediateWithoutProration = 3,
Deferred = 4
}
}

0 comments on commit b2edd3e

Please sign in to comment.