From 409f4f2ff35230745ad07d4c7a3c6261c4eeb818 Mon Sep 17 00:00:00 2001 From: Radoslaw Kubas Date: Sun, 15 Aug 2021 11:20:13 +0200 Subject: [PATCH 1/2] subscription downgrade / upgrade (Google Play Billing Library 4.0.0 compatible) --- .../InAppBilling.android.cs | 93 +++++++++++++++++++ src/Plugin.InAppBilling/InAppBilling.apple.cs | 10 ++ src/Plugin.InAppBilling/InAppBilling.uwp.cs | 9 ++ .../Shared/BaseInAppBilling.shared.cs | 10 ++ .../Shared/IInAppBilling.shared.cs | 11 +++ 5 files changed, 133 insertions(+) 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. /// From 002655ccbe1fc337b6164157dc411f9207498ae9 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Wed, 8 Sep 2021 10:19:40 -0700 Subject: [PATCH 2/2] Update to an enum and remove verify as not needed --- .../InAppBilling.android.cs | 18 +++++------------- src/Plugin.InAppBilling/InAppBilling.apple.cs | 4 +--- src/Plugin.InAppBilling/InAppBilling.uwp.cs | 4 +--- .../Shared/BaseInAppBilling.shared.cs | 3 +-- .../Shared/IInAppBilling.shared.cs | 2 +- .../Shared/ItemType.shared.cs | 11 +++++++++++ 6 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/Plugin.InAppBilling/InAppBilling.android.cs b/src/Plugin.InAppBilling/InAppBilling.android.cs index 9df4edc..3979552 100644 --- a/src/Plugin.InAppBilling/InAppBilling.android.cs +++ b/src/Plugin.InAppBilling/InAppBilling.android.cs @@ -206,9 +206,8 @@ public override Task> GetPurchasesAsync(ItemTy /// 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) + public override async Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription,SubscriptionProrationMode prorationMode = SubscriptionProrationMode.ImmediateWithTimeProration) { if (BillingClient == null || !IsConnected) { @@ -222,12 +221,12 @@ public override async Task UpgradePurchasedSubscriptionAsy return null; } - var purchase = await UpgradePurchasedSubscriptionInternalAsync(newProductId, purchaseTokenOfOriginalSubscription, prorationMode, verifyPurchase); + var purchase = await UpgradePurchasedSubscriptionInternalAsync(newProductId, purchaseTokenOfOriginalSubscription, prorationMode); return purchase; } - async Task UpgradePurchasedSubscriptionInternalAsync(string newProductId, string purchaseTokenOfOriginalSubscription, int prorationMode = 1, IInAppBillingVerifyPurchase verifyPurchase = null) + async Task UpgradePurchasedSubscriptionInternalAsync(string newProductId, string purchaseTokenOfOriginalSubscription, SubscriptionProrationMode prorationMode) { var itemType = BillingClient.SkuType.Subs; @@ -256,7 +255,7 @@ async Task UpgradePurchasedSubscriptionInternalAsync(strin var updateParams = BillingFlowParams.SubscriptionUpdateParams.NewBuilder() .SetOldSkuPurchaseToken(purchaseTokenOfOriginalSubscription) - .SetReplaceSkusProrationMode(prorationMode) + .SetReplaceSkusProrationMode((int)prorationMode) .Build(); var flowParams = BillingFlowParams.NewBuilder() @@ -282,14 +281,7 @@ async Task UpgradePurchasedSubscriptionInternalAsync(strin 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; + return androidPurchase.ToIABPurchase(); } /// diff --git a/src/Plugin.InAppBilling/InAppBilling.apple.cs b/src/Plugin.InAppBilling/InAppBilling.apple.cs index 2c503c2..272fea5 100644 --- a/src/Plugin.InAppBilling/InAppBilling.apple.cs +++ b/src/Plugin.InAppBilling/InAppBilling.apple.cs @@ -209,10 +209,8 @@ public async override Task PurchaseAsync(string productId, /// (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) - { + 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 diff --git a/src/Plugin.InAppBilling/InAppBilling.uwp.cs b/src/Plugin.InAppBilling/InAppBilling.uwp.cs index 32dedbe..31251ee 100644 --- a/src/Plugin.InAppBilling/InAppBilling.uwp.cs +++ b/src/Plugin.InAppBilling/InAppBilling.uwp.cs @@ -99,10 +99,8 @@ 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) - { + 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 7e817d2..4bc6dfd 100644 --- a/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs +++ b/src/Plugin.InAppBilling/Shared/BaseInAppBilling.shared.cs @@ -66,9 +66,8 @@ public abstract class BaseInAppBilling : IInAppBilling, IDisposable /// 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); + 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 eea7b01..7f36b66 100644 --- a/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs +++ b/src/Plugin.InAppBilling/Shared/IInAppBilling.shared.cs @@ -71,7 +71,7 @@ public interface IInAppBilling : IDisposable /// Verify Purchase implementation /// Purchase details /// If an error occures during processing - Task UpgradePurchasedSubscriptionAsync(string newProductId, string purchaseTokenOfOriginalSubscription, int prorationMode = 1, IInAppBillingVerifyPurchase verifyPurchase = null); + 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 + } }