diff --git a/src/Plugin.InAppBilling/InAppBilling.android.cs b/src/Plugin.InAppBilling/InAppBilling.android.cs index df7cf8e..9df4edc 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -199,6 +199,99 @@ 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) + /// Verify Purchase implementation + /// Purchase details + public override async Task 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 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 { 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 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; + } + /// /// Purchase a specific product or subscription /// diff --git a/src/Plugin.InAppBilling/InAppBilling.apple.cs b/src/Plugin.InAppBilling/InAppBilling.apple.cs index a63b637..2c503c2 100644 --- a/src/Plugin.InAppBilling/InAppBilling.apple.cs +++ b/src/Plugin.InAppBilling/InAppBilling.apple.cs @@ -205,6 +205,16 @@ 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, 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 diff --git a/src/Plugin.InAppBilling/InAppBilling.uwp.cs b/src/Plugin.InAppBilling/InAppBilling.uwp.cs index 95fed78..32dedbe 100644 --- a/src/Plugin.InAppBilling/InAppBilling.uwp.cs +++ b/src/Plugin.InAppBilling/InAppBilling.uwp.cs @@ -95,6 +95,15 @@ 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, int prorationMode = 1, IInAppBillingVerifyPurchase verifyPurchase = null) + { + 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..7e817d2 100644 --- a/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs +++ b/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs @@ -60,6 +60,16 @@ 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 + /// Verify Purchase implementation + /// Purchase details + public abstract Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, int prorationMode = 1, IInAppBillingVerifyPurchase verifyPurchase = null); + /// /// 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..eea7b01 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, int prorationMode = 1, IInAppBillingVerifyPurchase verifyPurchase = null); + /// /// Consume a purchase with a purchase token. ///