diff --git a/src/Plugin.InAppBilling/InAppBilling.android.cs b/src/Plugin.InAppBilling/InAppBilling.android.cs index df7cf8e..3979552 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -199,6 +199,91 @@ public override Task> GetPurchasesAsync(ItemTy return Task.FromResult(purchasesResult.PurchasesList.Select(p => p.ToIABPurchase())); } + /// + /// (Android specific) Upgrade/Downgrade/Change a previously purchased subscription + /// + /// Sku or ID of product that will replace the old one + /// Sku or ID of product that needs to be upgraded + /// 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) + { + 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 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 { 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 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(); + } + /// /// Purchase a specific product or subscription /// diff --git a/src/Plugin.InAppBilling/InAppBilling.apple.cs b/src/Plugin.InAppBilling/InAppBilling.apple.cs index a63b637..272fea5 100644 --- a/src/Plugin.InAppBilling/InAppBilling.apple.cs +++ b/src/Plugin.InAppBilling/InAppBilling.apple.cs @@ -205,6 +205,14 @@ public async override Task PurchaseAsync(string productId, return purchase; } + /// + /// (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) => + throw new NotImplementedException("iOS not supported. Apple store manages upgrades natively when subscriptions of the same group are purchased."); + + public override string ReceiptData { get diff --git a/src/Plugin.InAppBilling/InAppBilling.uwp.cs b/src/Plugin.InAppBilling/InAppBilling.uwp.cs index 95fed78..31251ee 100644 --- a/src/Plugin.InAppBilling/InAppBilling.uwp.cs +++ b/src/Plugin.InAppBilling/InAppBilling.uwp.cs @@ -95,6 +95,13 @@ 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) => + throw new NotImplementedException("UWP not supported."); + /// /// Consume a purchase with a purchase token. /// diff --git a/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs b/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs index f019976..4bc6dfd 100644 --- a/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs +++ b/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs @@ -60,6 +60,15 @@ public abstract class BaseInAppBilling : IInAppBilling, IDisposable /// If an error occures during processing public abstract Task PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null); + /// + /// (Android specific) Upgrade/Downgrade a previously purchased subscription + /// + /// Sku or ID of product that will replace the old one + /// 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); + /// /// 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 79d22ac..7f36b66 100644 --- a/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs +++ b/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs @@ -62,6 +62,17 @@ public interface IInAppBilling : IDisposable /// If an error occures during processing Task PurchaseAsync(string productId, ItemType itemType, string obfuscatedAccountId = null, string obfuscatedProfileId = null); + /// + /// (Android specific) Upgrade/Downgrade a previously purchased subscription + /// + /// Sku or ID of product that will replace the old one + /// Purchase token of original subscription (can not be null) + /// Proration mode (1 - ImmediateWithTimeProration, 2 - ImmediateAndChargeProratedPrice, 3 - ImmediateWithoutProration, 4 - Deferred) + /// Verify Purchase implementation + /// Purchase details + /// If an error occures during processing + Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration); + /// /// Consume a purchase with a purchase token. /// diff --git a/src/Plugin.InAppBilling/Shared/ItemType.shared.cs b/src/Plugin.InAppBilling/Shared/ItemType.shared.cs index 208c640..394cd95 100644 --- a/src/Plugin.InAppBilling/Shared/ItemType.shared.cs +++ b/src/Plugin.InAppBilling/Shared/ItemType.shared.cs @@ -20,4 +20,15 @@ public enum ItemType /// Subscription } + + /// + /// Subcription proration mode + /// + public enum SubscriptionProrationMode + { + ImmediateWithTimeProration = 1, + ImmediateAndChargeProratedPrice = 2, + ImmediateWithoutProration = 3, + Deferred = 4 + } }