From 3c3047a80f9050791d7b7ae9637163337ae54ec3 Mon Sep 17 00:00:00 2001 From: Alessandro Agosto Date: Wed, 29 Apr 2020 23:04:11 +0200 Subject: [PATCH] Add support to cross-grade a subscription on Android This change introduces basic support to replace an old subscription on Android. On iOS passing the added parameter will have no effect. Please note that the current implementation is basic, in that it misses a way to contextually change the proration mode. It also uses `setOldSku(string)` rather than `setOldSku(string, string)` which also requires the old purchaseToken to be provided. Ref: https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setoldsku --- AUTHORS | 3 +- packages/in_app_purchase/CHANGELOG.md | 6 +++- .../inapppurchase/MethodCallHandlerImpl.java | 36 ++++++++++++++++--- .../billing_client_wrapper.dart | 3 +- .../google_play_connection.dart | 3 +- .../src/in_app_purchase/purchase_details.dart | 7 +++- packages/in_app_purchase/pubspec.yaml | 2 +- .../billing_client_wrapper_test.dart | 27 ++++++++++++++ 8 files changed, 77 insertions(+), 10 deletions(-) diff --git a/AUTHORS b/AUTHORS index b27c156188f8..34ab4e8bdafb 100644 --- a/AUTHORS +++ b/AUTHORS @@ -56,4 +56,5 @@ Giancarlo Rocha Ryo Miyake Théo Champion Kazuki Yamaguchi -Eitan Schwartz \ No newline at end of file +Eitan Schwartz +Alessandro Agosto \ No newline at end of file diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md index 647ff0bec4c1..b57ec20fdde7 100644 --- a/packages/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.4 + +* Android: Add support for subscription cross-grades + ## 0.3.3 * Introduce `SKPaymentQueueWrapper.transactions`. @@ -9,7 +13,7 @@ ## 0.3.2+1 * iOS: Fix only transactions with SKPaymentTransactionStatePurchased and SKPaymentTransactionStateFailed can be finished. -* iOS: Only one pending transaction of a given product is allowed. +* iOS: Only one pending transaction of a given product is allowed. ## 0.3.2 diff --git a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index 335d4b8e12cf..ba9ba294bc4e 100644 --- a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -120,7 +120,10 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { break; case InAppPurchasePlugin.MethodNames.LAUNCH_BILLING_FLOW: launchBillingFlow( - (String) call.argument("sku"), (String) call.argument("accountId"), result); + (String) call.argument("sku"), + (String) call.argument("accountId"), + (String) call.argument("oldSku"), + result); break; case InAppPurchasePlugin.MethodNames.QUERY_PURCHASES: queryPurchases((String) call.argument("skuType"), result); @@ -189,7 +192,10 @@ public void onSkuDetailsResponse( } private void launchBillingFlow( - String sku, @Nullable String accountId, MethodChannel.Result result) { + String sku, + @Nullable String accountId, + @Nullable String oldSku, + MethodChannel.Result result) { if (billingClientError(result)) { return; } @@ -203,6 +209,19 @@ private void launchBillingFlow( return; } + SkuDetails oldSkuDetails = null; + + if (oldSku != null) { + oldSkuDetails = cachedSkus.get(oldSku); + if (oldSkuDetails == null) { + result.error( + "NOT_FOUND", + "Details for sku " + oldSku + " are not available. Has this ID already been fetched?", + null); + return; + } + } + if (activity == null) { result.error( "ACTIVITY_UNAVAILABLE", @@ -213,8 +232,17 @@ private void launchBillingFlow( return; } - BillingFlowParams.Builder paramsBuilder = - BillingFlowParams.newBuilder().setSkuDetails(skuDetails); + BillingFlowParams.Builder paramsBuilder; + + // Requested a subscription cross-grade. + if (oldSkuDetails != null) { + // NOTE: currently only the default proration mode is supported. + // https://developer.android.com/google/play/billing/billing_subscriptions#set-proration-mode + paramsBuilder = BillingFlowParams.newBuilder().setSkuDetails(skuDetails).setOldSku(oldSku); + } else { + paramsBuilder = BillingFlowParams.newBuilder().setSkuDetails(skuDetails); + } + if (accountId != null && !accountId.isEmpty()) { paramsBuilder.setAccountId(accountId); } diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/billing_client_wrapper.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/billing_client_wrapper.dart index ebbd90aba0f4..594cd619c2e2 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/billing_client_wrapper.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/billing_client_wrapper.dart @@ -168,11 +168,12 @@ class BillingClient { /// and [the given /// accountId](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder.html#setAccountId(java.lang.String)). Future launchBillingFlow( - {@required String sku, String accountId}) async { + {@required String sku, String accountId, String oldSku}) async { assert(sku != null); final Map arguments = { 'sku': sku, 'accountId': accountId, + 'oldSku': oldSku, }; return BillingResultWrapper.fromJson( await channel.invokeMapMethod( diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart b/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart index 581a7bd9f8fe..aa4575898fc3 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart @@ -56,7 +56,8 @@ class GooglePlayConnection BillingResultWrapper billingResultWrapper = await billingClient.launchBillingFlow( sku: purchaseParam.productDetails.id, - accountId: purchaseParam.applicationUserName); + accountId: purchaseParam.applicationUserName, + oldSku: purchaseParam.oldProduct?.id); return billingResultWrapper.responseCode == BillingResponse.ok; } diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart b/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart index e9dca786b4b6..8845df554666 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart @@ -79,7 +79,8 @@ class PurchaseParam { PurchaseParam( {@required this.productDetails, this.applicationUserName, - this.sandboxTesting}); + this.sandboxTesting, + this.oldProduct}); /// The product to create payment for. /// @@ -96,6 +97,10 @@ class PurchaseParam { /// The 'sandboxTesting' is only available on iOS, set it to `true` for testing in AppStore's sandbox environment. The default value is `false`. final bool sandboxTesting; + + /// The 'oldProduct' is only available on Android for non-consumable resources and is meant to allow subscription cross-grades. + /// By setting this you will replace the subscription represented by `oldProduct`. On iOS this is ignored. + final ProductDetails oldProduct; } /// Represents the transaction details of a purchase. diff --git a/packages/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/pubspec.yaml index ac8971e74903..12c7b45f7ddf 100644 --- a/packages/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/pubspec.yaml @@ -1,7 +1,7 @@ name: in_app_purchase description: A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play. homepage: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase -version: 0.3.3 +version: 0.3.4 dependencies: async: ^2.0.8 diff --git a/packages/in_app_purchase/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/test/billing_client_wrappers/billing_client_wrapper_test.dart index 54f7c3eda77f..9df1bc7f1926 100644 --- a/packages/in_app_purchase/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -159,6 +159,7 @@ void main() { stubPlatform.previousCallMatching(launchMethodName).arguments; expect(arguments['sku'], equals(skuDetails.sku)); expect(arguments['accountId'], equals(accountId)); + expect(arguments['oldSku'], isNull); }); test('handles null accountId', () async { @@ -178,6 +179,32 @@ void main() { stubPlatform.previousCallMatching(launchMethodName).arguments; expect(arguments['sku'], equals(skuDetails.sku)); expect(arguments['accountId'], isNull); + expect(arguments['oldSku'], isNull); + }); + + test('handles oldSku', () async { + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + ); + final SkuDetailsWrapper skuDetails = dummySkuDetails; + final String accountId = "hashedAccountId"; + + expect( + await billingClient.launchBillingFlow( + sku: skuDetails.sku, + accountId: accountId, + oldSku: skuDetails.sku), + equals(expectedBillingResult)); + Map arguments = + stubPlatform.previousCallMatching(launchMethodName).arguments; + expect(arguments['sku'], equals(skuDetails.sku)); + expect(arguments['accountId'], equals(accountId)); + expect(arguments['oldSku'], equals(skuDetails.sku)); }); });