[in_app_purchase] billingClient launchPriceChangeConfirmationFlow (#4077)

diff --git a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md
index 9066fab..d41032d 100644
--- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md
+++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.1.4
+
+* Added support for launchPriceChangeConfirmationFlow in the BillingClientWrapper and in InAppPurchaseAndroidPlatformAddition.
+
 ## 0.1.3+1
 
 * Add payment proxy.
diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java
index b968805..b21ab69 100644
--- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java
+++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java
@@ -46,6 +46,8 @@
     static final String ACKNOWLEDGE_PURCHASE =
         "BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)";
     static final String IS_FEATURE_SUPPORTED = "BillingClient#isFeatureSupported(String)";
+    static final String LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW =
+        "BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)";
 
     private MethodNames() {};
   }
diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java
index 473c21d..7803318 100644
--- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java
+++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java
@@ -24,6 +24,7 @@
 import com.android.billingclient.api.BillingResult;
 import com.android.billingclient.api.ConsumeParams;
 import com.android.billingclient.api.ConsumeResponseListener;
+import com.android.billingclient.api.PriceChangeFlowParams;
 import com.android.billingclient.api.PurchaseHistoryRecord;
 import com.android.billingclient.api.PurchaseHistoryResponseListener;
 import com.android.billingclient.api.SkuDetails;
@@ -41,7 +42,7 @@
 
   private static final String TAG = "InAppPurchasePlugin";
   private static final String LOAD_SKU_DOC_URL =
-      "https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/README.md#loading-products-for-sale";
+      "https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/in_app_purchase/README.md#loading-products-for-sale";
 
   @Nullable private BillingClient billingClient;
   private final BillingClientFactory billingClientFactory;
@@ -148,6 +149,9 @@
       case InAppPurchasePlugin.MethodNames.IS_FEATURE_SUPPORTED:
         isFeatureSupported((String) call.argument("feature"), result);
         break;
+      case InAppPurchasePlugin.MethodNames.LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW:
+        launchPriceChangeConfirmationFlow((String) call.argument("sku"), result);
+        break;
       default:
         result.notImplemented();
     }
@@ -374,6 +378,44 @@
     }
   }
 
+  private void launchPriceChangeConfirmationFlow(String sku, MethodChannel.Result result) {
+    if (activity == null) {
+      result.error(
+          "ACTIVITY_UNAVAILABLE",
+          "launchPriceChangeConfirmationFlow is not available. "
+              + "This method must be run with the app in foreground.",
+          null);
+      return;
+    }
+    if (billingClientError(result)) {
+      return;
+    }
+    // Note that assert doesn't work on Android (see https://stackoverflow.com/a/6176529/5167831 and https://stackoverflow.com/a/8164195/5167831)
+    // and that this assert is only added to silence the analyser. The actual null check
+    // is handled by the `billingClientError()` call.
+    assert billingClient != null;
+
+    SkuDetails skuDetails = cachedSkus.get(sku);
+    if (skuDetails == null) {
+      result.error(
+          "NOT_FOUND",
+          String.format(
+              "Details for sku %s are not available. It might because skus were not fetched prior to the call. Please fetch the skus first. An example of how to fetch the skus could be found here: %s",
+              sku, LOAD_SKU_DOC_URL),
+          null);
+      return;
+    }
+
+    PriceChangeFlowParams params =
+        new PriceChangeFlowParams.Builder().setSkuDetails(skuDetails).build();
+    billingClient.launchPriceChangeConfirmationFlow(
+        activity,
+        params,
+        billingResult -> {
+          result.success(Translator.fromBillingResult(billingResult));
+        });
+  }
+
   private boolean billingClientError(MethodChannel.Result result) {
     if (billingClient != null) {
       return false;
diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java
index 7465e6a..6f9256c 100644
--- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java
+++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java
@@ -10,6 +10,7 @@
 import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.IS_FEATURE_SUPPORTED;
 import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.IS_READY;
 import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.LAUNCH_BILLING_FLOW;
+import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW;
 import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ON_DISCONNECT;
 import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ON_PURCHASES_UPDATED;
 import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_PURCHASES;
@@ -52,6 +53,8 @@
 import com.android.billingclient.api.BillingResult;
 import com.android.billingclient.api.ConsumeParams;
 import com.android.billingclient.api.ConsumeResponseListener;
+import com.android.billingclient.api.PriceChangeConfirmationListener;
+import com.android.billingclient.api.PriceChangeFlowParams;
 import com.android.billingclient.api.Purchase;
 import com.android.billingclient.api.Purchase.PurchasesResult;
 import com.android.billingclient.api.PurchaseHistoryRecord;
@@ -722,7 +725,7 @@
   }
 
   @Test
-  public void endConnection_if_activity_dettached() {
+  public void endConnection_if_activity_detached() {
     InAppPurchasePlugin plugin = new InAppPurchasePlugin();
     plugin.setMethodCallHandler(methodChannelHandler);
     mockStartConnection();
@@ -768,6 +771,97 @@
     verify(result).success(false);
   }
 
+  @Test
+  public void launchPriceChangeConfirmationFlow() {
+    // Set up the sku details
+    establishConnectedBillingClient(null, null);
+    String skuId = "foo";
+    queryForSkus(singletonList(skuId));
+
+    BillingResult billingResult =
+        BillingResult.newBuilder()
+            .setResponseCode(BillingClient.BillingResponseCode.OK)
+            .setDebugMessage("dummy debug message")
+            .build();
+
+    // Set up the mock billing client
+    ArgumentCaptor<PriceChangeConfirmationListener> priceChangeConfirmationListenerArgumentCaptor =
+        ArgumentCaptor.forClass(PriceChangeConfirmationListener.class);
+    ArgumentCaptor<PriceChangeFlowParams> priceChangeFlowParamsArgumentCaptor =
+        ArgumentCaptor.forClass(PriceChangeFlowParams.class);
+    doNothing()
+        .when(mockBillingClient)
+        .launchPriceChangeConfirmationFlow(
+            any(),
+            priceChangeFlowParamsArgumentCaptor.capture(),
+            priceChangeConfirmationListenerArgumentCaptor.capture());
+
+    // Call the methodChannelHandler
+    HashMap<String, Object> arguments = new HashMap<>();
+    arguments.put("sku", skuId);
+    methodChannelHandler.onMethodCall(
+        new MethodCall(LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW, arguments), result);
+
+    // Verify the price change params.
+    PriceChangeFlowParams priceChangeFlowParams = priceChangeFlowParamsArgumentCaptor.getValue();
+    assertEquals(skuId, priceChangeFlowParams.getSkuDetails().getSku());
+
+    // Set the response in the callback
+    PriceChangeConfirmationListener priceChangeConfirmationListener =
+        priceChangeConfirmationListenerArgumentCaptor.getValue();
+    priceChangeConfirmationListener.onPriceChangeConfirmationResult(billingResult);
+
+    // Verify we pass the response to result
+    verify(result, never()).error(any(), any(), any());
+    ArgumentCaptor<HashMap> resultCaptor = ArgumentCaptor.forClass(HashMap.class);
+    verify(result, times(1)).success(resultCaptor.capture());
+    assertEquals(fromBillingResult(billingResult), resultCaptor.getValue());
+  }
+
+  @Test
+  public void launchPriceChangeConfirmationFlow_withoutActivity_returnsActivityUnavailableError() {
+    // Set up the sku details
+    establishConnectedBillingClient(null, null);
+    String skuId = "foo";
+    queryForSkus(singletonList(skuId));
+
+    methodChannelHandler.setActivity(null);
+
+    // Call the methodChannelHandler
+    HashMap<String, Object> arguments = new HashMap<>();
+    arguments.put("sku", skuId);
+    methodChannelHandler.onMethodCall(
+        new MethodCall(LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW, arguments), result);
+    verify(result, times(1)).error(eq("ACTIVITY_UNAVAILABLE"), any(), any());
+  }
+
+  @Test
+  public void launchPriceChangeConfirmationFlow_withoutSkuQuery_returnsNotFoundError() {
+    // Set up the sku details
+    establishConnectedBillingClient(null, null);
+    String skuId = "foo";
+
+    // Call the methodChannelHandler
+    HashMap<String, Object> arguments = new HashMap<>();
+    arguments.put("sku", skuId);
+    methodChannelHandler.onMethodCall(
+        new MethodCall(LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW, arguments), result);
+    verify(result, times(1)).error(eq("NOT_FOUND"), contains("sku"), any());
+  }
+
+  @Test
+  public void launchPriceChangeConfirmationFlow_withoutBillingClient_returnsUnavailableError() {
+    // Set up the sku details
+    String skuId = "foo";
+
+    // Call the methodChannelHandler
+    HashMap<String, Object> arguments = new HashMap<>();
+    arguments.put("sku", skuId);
+    methodChannelHandler.onMethodCall(
+        new MethodCall(LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW, arguments), result);
+    verify(result, times(1)).error(eq("UNAVAILABLE"), contains("BillingClient"), any());
+  }
+
   private ArgumentCaptor<BillingClientStateListener> mockStartConnection() {
     Map<String, Object> arguments = new HashMap<>();
     arguments.put("handle", 1);
diff --git a/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart b/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart
index cb8cb75..1267341 100644
--- a/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart
+++ b/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart
@@ -31,8 +31,8 @@
 
 const String _kConsumableId = 'consumable';
 const String _kUpgradeId = 'upgrade';
-const String _kSilverSubscriptionId = 'subscription_silver';
-const String _kGoldSubscriptionId = 'subscription_gold';
+const String _kSilverSubscriptionId = 'subscription_silver1';
+const String _kGoldSubscriptionId = 'subscription_gold1';
 const List<String> _kProductIds = <String>[
   _kConsumableId,
   _kUpgradeId,
@@ -251,7 +251,21 @@
               productDetails.description,
             ),
             trailing: previousPurchase != null
-                ? Icon(Icons.check)
+                ? IconButton(
+                    onPressed: () {
+                      final InAppPurchaseAndroidPlatformAddition addition =
+                          InAppPurchasePlatformAddition.instance
+                              as InAppPurchaseAndroidPlatformAddition;
+                      var skuDetails =
+                          (productDetails as GooglePlayProductDetails)
+                              .skuDetails;
+                      addition
+                          .launchPriceChangeConfirmationFlow(
+                              sku: skuDetails.sku)
+                          .then((value) => print(
+                              "confirmationResponse: ${value.responseCode}"));
+                    },
+                    icon: Icon(Icons.upgrade))
                 : TextButton(
                     child: Text(productDetails.price),
                     style: TextButton.styleFrom(
diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart
index cf08fa9..4393d1d 100644
--- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart
+++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart
@@ -311,6 +311,26 @@
     return result ?? false;
   }
 
+  /// Initiates a flow to confirm the change of price for an item subscribed by the user.
+  ///
+  /// When the price of a user subscribed item has changed, launch this flow to take users to
+  /// a screen with price change information. User can confirm the new price or cancel the flow.
+  ///
+  /// The skuDetails needs to have already been fetched in a [querySkuDetails]
+  /// call.
+  Future<BillingResultWrapper> launchPriceChangeConfirmationFlow(
+      {required String sku}) async {
+    assert(sku != null);
+    final Map<String, dynamic> arguments = <String, dynamic>{
+      'sku': sku,
+    };
+    return BillingResultWrapper.fromJson((await channel.invokeMapMethod<String,
+                dynamic>(
+            'BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)',
+            arguments)) ??
+        <String, dynamic>{});
+  }
+
   /// The method call handler for [channel].
   @visibleForTesting
   Future<void> callHandler(MethodCall call) async {
diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart
index fc4ab7c..11b105a 100644
--- a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart
+++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart
@@ -141,4 +141,16 @@
   Future<bool> isFeatureSupported(BillingClientFeature feature) async {
     return _billingClient.isFeatureSupported(feature);
   }
+
+  /// Initiates a flow to confirm the change of price for an item subscribed by the user.
+  ///
+  /// When the price of a user subscribed item has changed, launch this flow to take users to
+  /// a screen with price change information. User can confirm the new price or cancel the flow.
+  ///
+  /// The skuDetails needs to have already been fetched in a
+  /// [InAppPurchaseAndroidPlatform.queryProductDetails] call.
+  Future<BillingResultWrapper> launchPriceChangeConfirmationFlow(
+      {required String sku}) {
+    return _billingClient.launchPriceChangeConfirmationFlow(sku: sku);
+  }
 }
diff --git a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml
index ad06d85..4f11cdf 100644
--- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml
+++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml
@@ -2,7 +2,7 @@
 description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs.
 repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_android
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22
-version: 0.1.3+1
+version: 0.1.4
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart
index 6ab1641..02ae9ba 100644
--- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart
+++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart
@@ -574,4 +574,44 @@
       expect(arguments['feature'], equals('subscriptions'));
     });
   });
+
+  group('launchPriceChangeConfirmationFlow', () {
+    const String launchPriceChangeConfirmationFlowMethodName =
+        'BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)';
+
+    final expectedBillingResultPriceChangeConfirmation = BillingResultWrapper(
+      responseCode: BillingResponse.ok,
+      debugMessage: 'dummy message',
+    );
+
+    test('serializes and deserializes data', () async {
+      stubPlatform.addResponse(
+        name: launchPriceChangeConfirmationFlowMethodName,
+        value:
+            buildBillingResultMap(expectedBillingResultPriceChangeConfirmation),
+      );
+
+      expect(
+        await billingClient.launchPriceChangeConfirmationFlow(
+          sku: dummySkuDetails.sku,
+        ),
+        equals(expectedBillingResultPriceChangeConfirmation),
+      );
+    });
+
+    test('passes sku to launchPriceChangeConfirmationFlow', () async {
+      stubPlatform.addResponse(
+        name: launchPriceChangeConfirmationFlowMethodName,
+        value:
+            buildBillingResultMap(expectedBillingResultPriceChangeConfirmation),
+      );
+      await billingClient.launchPriceChangeConfirmationFlow(
+        sku: dummySkuDetails.sku,
+      );
+      final MethodCall call = stubPlatform
+          .previousCallMatching(launchPriceChangeConfirmationFlowMethodName);
+      expect(call.arguments,
+          equals(<dynamic, dynamic>{'sku': dummySkuDetails.sku}));
+    });
+  });
 }
diff --git a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart
index 0ef17e7..a478cab 100644
--- a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart
+++ b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart
@@ -171,4 +171,44 @@
       expect(arguments['feature'], equals('subscriptions'));
     });
   });
+
+  group('launchPriceChangeConfirmationFlow', () {
+    const String launchPriceChangeConfirmationFlowMethodName =
+        'BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)';
+    const dummySku = 'sku';
+
+    final expectedBillingResultPriceChangeConfirmation = BillingResultWrapper(
+      responseCode: BillingResponse.ok,
+      debugMessage: 'dummy message',
+    );
+
+    test('serializes and deserializes data', () async {
+      stubPlatform.addResponse(
+        name: launchPriceChangeConfirmationFlowMethodName,
+        value:
+            buildBillingResultMap(expectedBillingResultPriceChangeConfirmation),
+      );
+
+      expect(
+        await iapAndroidPlatformAddition.launchPriceChangeConfirmationFlow(
+          sku: dummySku,
+        ),
+        equals(expectedBillingResultPriceChangeConfirmation),
+      );
+    });
+
+    test('passes sku to launchPriceChangeConfirmationFlow', () async {
+      stubPlatform.addResponse(
+        name: launchPriceChangeConfirmationFlowMethodName,
+        value:
+            buildBillingResultMap(expectedBillingResultPriceChangeConfirmation),
+      );
+      await iapAndroidPlatformAddition.launchPriceChangeConfirmationFlow(
+        sku: dummySku,
+      );
+      final MethodCall call = stubPlatform
+          .previousCallMatching(launchPriceChangeConfirmationFlowMethodName);
+      expect(call.arguments, equals(<dynamic, dynamic>{'sku': dummySku}));
+    });
+  });
 }