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)); }); });