[in_app_purchase] Added BillingClient.isFeatureSupported (#4063)

* Added BillingClient.isFeatureSupported

* pubspec and changelog
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 6175462..526fdba 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.3
+
+Added support for isFeatureSupported in the BillingClientWrapper and in InAppPurchaseAndroidPlatformAddition.
+
 ## 0.1.2
 
 * Added support for the obfuscatedAccountId and obfuscatedProfileId in the PurchaseWrapper.
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 e4719f0..ad53439 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
@@ -38,6 +38,7 @@
         "BillingClient#consumeAsync(String, ConsumeResponseListener)";
     static final String ACKNOWLEDGE_PURCHASE =
         "BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)";
+    static final String IS_FEATURE_SUPPORTED = "BillingClient#isFeatureSupported(String)";
 
     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 cfcb81a..473c21d 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
@@ -145,6 +145,9 @@
       case InAppPurchasePlugin.MethodNames.ACKNOWLEDGE_PURCHASE:
         acknowledgePurchase((String) call.argument("purchaseToken"), result);
         break;
+      case InAppPurchasePlugin.MethodNames.IS_FEATURE_SUPPORTED:
+        isFeatureSupported((String) call.argument("feature"), result);
+        break;
       default:
         result.notImplemented();
     }
@@ -379,4 +382,13 @@
     result.error("UNAVAILABLE", "BillingClient is unset. Try reconnecting.", null);
     return true;
   }
+
+  private void isFeatureSupported(String feature, MethodChannel.Result result) {
+    if (billingClientError(result)) {
+      return;
+    }
+    assert billingClient != null;
+    BillingResult billingResult = billingClient.isFeatureSupported(feature);
+    result.success(billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK);
+  }
 }
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 4d7a022..7465e6a 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
@@ -7,6 +7,7 @@
 import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ACKNOWLEDGE_PURCHASE;
 import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.CONSUME_PURCHASE_ASYNC;
 import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.END_CONNECTION;
+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.ON_DISCONNECT;
@@ -729,6 +730,44 @@
     verify(mockBillingClient).endConnection();
   }
 
+  @Test
+  public void isFutureSupported_true() {
+    mockStartConnection();
+    final String feature = "subscriptions";
+    Map<String, Object> arguments = new HashMap<>();
+    arguments.put("feature", feature);
+
+    BillingResult billingResult =
+        BillingResult.newBuilder()
+            .setResponseCode(BillingClient.BillingResponseCode.OK)
+            .setDebugMessage("dummy debug message")
+            .build();
+
+    MethodCall call = new MethodCall(IS_FEATURE_SUPPORTED, arguments);
+    when(mockBillingClient.isFeatureSupported(feature)).thenReturn(billingResult);
+    methodChannelHandler.onMethodCall(call, result);
+    verify(result).success(true);
+  }
+
+  @Test
+  public void isFutureSupported_false() {
+    mockStartConnection();
+    final String feature = "subscriptions";
+    Map<String, Object> arguments = new HashMap<>();
+    arguments.put("feature", feature);
+
+    BillingResult billingResult =
+        BillingResult.newBuilder()
+            .setResponseCode(BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED)
+            .setDebugMessage("dummy debug message")
+            .build();
+
+    MethodCall call = new MethodCall(IS_FEATURE_SUPPORTED, arguments);
+    when(mockBillingClient.isFeatureSupported(feature)).thenReturn(billingResult);
+    methodChannelHandler.onMethodCall(call, result);
+    verify(result).success(false);
+  }
+
   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 c5726c4..cb8cb75 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
@@ -147,6 +147,7 @@
             _buildConnectionCheckTile(),
             _buildProductList(),
             _buildConsumableBox(),
+            _FeatureCard(),
           ],
         ),
       );
@@ -434,3 +435,59 @@
     return oldSubscription;
   }
 }
+
+class _FeatureCard extends StatelessWidget {
+  final InAppPurchaseAndroidPlatformAddition addition =
+      InAppPurchasePlatformAddition.instance
+          as InAppPurchaseAndroidPlatformAddition;
+
+  _FeatureCard({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Card(
+        child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: <Widget>[
+          ListTile(title: Text('Available features')),
+          Divider(),
+          for (BillingClientFeature feature in BillingClientFeature.values)
+            _buildFeatureWidget(feature),
+        ]));
+  }
+
+  Widget _buildFeatureWidget(BillingClientFeature feature) {
+    return FutureBuilder<bool>(
+      future: addition.isFeatureSupported(feature),
+      builder: (context, snapshot) {
+        Color color = Colors.grey;
+        bool? data = snapshot.data;
+        if (data != null) {
+          color = data ? Colors.green : Colors.red;
+        }
+        return Padding(
+          padding: const EdgeInsets.fromLTRB(16.0, 4.0, 16.0, 4.0),
+          child: Text(
+            _featureToString(feature),
+            style: TextStyle(color: color),
+          ),
+        );
+      },
+    );
+  }
+
+  String _featureToString(BillingClientFeature feature) {
+    switch (feature) {
+      case BillingClientFeature.inAppItemsOnVR:
+        return 'inAppItemsOnVR';
+      case BillingClientFeature.priceChangeConfirmation:
+        return 'priceChangeConfirmation';
+      case BillingClientFeature.subscriptions:
+        return 'subscriptions';
+      case BillingClientFeature.subscriptionsOnVR:
+        return 'subscriptionsOnVR';
+      case BillingClientFeature.subscriptionsUpdate:
+        return 'subscriptionsUpdate';
+    }
+  }
+}
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 1f43b3a..cf08fa9 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
@@ -301,6 +301,16 @@
         <String, dynamic>{});
   }
 
+  /// Checks if the specified feature or capability is supported by the Play Store.
+  /// Call this to check if a [BillingClientFeature] is supported by the device.
+  Future<bool> isFeatureSupported(BillingClientFeature feature) async {
+    var result = await channel.invokeMethod<bool>(
+        'BillingClient#isFeatureSupported(String)', <String, dynamic>{
+      'feature': BillingClientFeatureConverter().toJson(feature),
+    });
+    return result ?? false;
+  }
+
   /// The method call handler for [channel].
   @visibleForTesting
   Future<void> callHandler(MethodCall call) async {
@@ -446,3 +456,31 @@
   @JsonValue(4)
   deferred,
 }
+
+/// Features/capabilities supported by [BillingClient.isFeatureSupported()](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.FeatureType).
+enum BillingClientFeature {
+  // WARNING: Changes to this class need to be reflected in our generated code.
+  // Run `flutter packages pub run build_runner watch` to rebuild and watch for
+  // further changes.
+
+  // JsonValues need to match constant values defined in https://developer.android.com/reference/com/android/billingclient/api/BillingClient.FeatureType#summary
+  /// Purchase/query for in-app items on VR.
+  @JsonValue('inAppItemsOnVr')
+  inAppItemsOnVR,
+
+  /// Launch a price change confirmation flow.
+  @JsonValue('priceChangeConfirmation')
+  priceChangeConfirmation,
+
+  /// Purchase/query for subscriptions.
+  @JsonValue('subscriptions')
+  subscriptions,
+
+  /// Purchase/query for subscriptions on VR.
+  @JsonValue('subscriptionsOnVr')
+  subscriptionsOnVR,
+
+  /// Subscriptions update/replace.
+  @JsonValue('subscriptionsUpdate')
+  subscriptionsUpdate
+}
diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.dart
index 46d6843..7ff3330 100644
--- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.dart
+++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.dart
@@ -72,15 +72,6 @@
   int toJson(ProrationMode object) => _$ProrationModeEnumMap[object]!;
 }
 
-// Define a class so we generate serializer helper methods for the enums
-@JsonSerializable()
-class _SerializedEnums {
-  late BillingResponse response;
-  late SkuType type;
-  late PurchaseStateWrapper purchaseState;
-  late ProrationMode prorationMode;
-}
-
 /// Serializer for [PurchaseStateWrapper].
 ///
 /// Use these in `@JsonSerializable()` classes by annotating them with
@@ -118,3 +109,34 @@
     }
   }
 }
+
+/// Serializer for [BillingClientFeature].
+///
+/// Use these in `@JsonSerializable()` classes by annotating them with
+/// `@BillingClientFeatureConverter()`.
+class BillingClientFeatureConverter
+    implements JsonConverter<BillingClientFeature, String> {
+  /// Default const constructor.
+  const BillingClientFeatureConverter();
+
+  @override
+  BillingClientFeature fromJson(String json) {
+    return _$enumDecode<BillingClientFeature, dynamic>(
+        _$BillingClientFeatureEnumMap.cast<BillingClientFeature, dynamic>(),
+        json);
+  }
+
+  @override
+  String toJson(BillingClientFeature object) =>
+      _$BillingClientFeatureEnumMap[object]!;
+}
+
+// Define a class so we generate serializer helper methods for the enums
+@JsonSerializable()
+class _SerializedEnums {
+  late BillingResponse response;
+  late SkuType type;
+  late PurchaseStateWrapper purchaseState;
+  late ProrationMode prorationMode;
+  late BillingClientFeature billingClientFeature;
+}
diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.g.dart
index 4186a2a..8d667d0 100644
--- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.g.dart
+++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.g.dart
@@ -13,7 +13,9 @@
     ..purchaseState =
         _$enumDecode(_$PurchaseStateWrapperEnumMap, json['purchaseState'])
     ..prorationMode =
-        _$enumDecode(_$ProrationModeEnumMap, json['prorationMode']);
+        _$enumDecode(_$ProrationModeEnumMap, json['prorationMode'])
+    ..billingClientFeature = _$enumDecode(
+        _$BillingClientFeatureEnumMap, json['billingClientFeature']);
 }
 
 Map<String, dynamic> _$_SerializedEnumsToJson(_SerializedEnums instance) =>
@@ -22,6 +24,8 @@
       'type': _$SkuTypeEnumMap[instance.type],
       'purchaseState': _$PurchaseStateWrapperEnumMap[instance.purchaseState],
       'prorationMode': _$ProrationModeEnumMap[instance.prorationMode],
+      'billingClientFeature':
+          _$BillingClientFeatureEnumMap[instance.billingClientFeature],
     };
 
 K _$enumDecode<K, V>(
@@ -83,3 +87,11 @@
   ProrationMode.immediateWithoutProration: 3,
   ProrationMode.deferred: 4,
 };
+
+const _$BillingClientFeatureEnumMap = {
+  BillingClientFeature.inAppItemsOnVR: 'inAppItemsOnVr',
+  BillingClientFeature.priceChangeConfirmation: 'priceChangeConfirmation',
+  BillingClientFeature.subscriptions: 'subscriptions',
+  BillingClientFeature.subscriptionsOnVR: 'subscriptionsOnVr',
+  BillingClientFeature.subscriptionsUpdate: 'subscriptionsUpdate',
+};
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 84f8b9e..fc4ab7c 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
@@ -135,4 +135,10 @@
     return QueryPurchaseDetailsResponse(
         pastPurchases: pastPurchases, error: error);
   }
+
+  /// Checks if the specified feature or capability is supported by the Play Store.
+  /// Call this to check if a [BillingClientFeature] is supported by the device.
+  Future<bool> isFeatureSupported(BillingClientFeature feature) async {
+    return _billingClient.isFeatureSupported(feature);
+  }
 }
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 900fa43..58069b0 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.2
+version: 0.1.3
 
 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 ec72897..6ab1641 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
@@ -544,4 +544,34 @@
               debugMessage: kInvalidBillingResultErrorMessage)));
     });
   });
+
+  group('isFeatureSupported', () {
+    const String isFeatureSupportedMethodName =
+        'BillingClient#isFeatureSupported(String)';
+    test('isFeatureSupported returns false', () async {
+      late Map<Object?, Object?> arguments;
+      stubPlatform.addResponse(
+        name: isFeatureSupportedMethodName,
+        value: false,
+        additionalStepBeforeReturn: (value) => arguments = value,
+      );
+      final bool isSupported = await billingClient
+          .isFeatureSupported(BillingClientFeature.subscriptions);
+      expect(isSupported, isFalse);
+      expect(arguments['feature'], equals('subscriptions'));
+    });
+
+    test('isFeatureSupported returns true', () async {
+      late Map<Object?, Object?> arguments;
+      stubPlatform.addResponse(
+        name: isFeatureSupportedMethodName,
+        value: true,
+        additionalStepBeforeReturn: (value) => arguments = value,
+      );
+      final bool isSupported = await billingClient
+          .isFeatureSupported(BillingClientFeature.subscriptions);
+      expect(isSupported, isTrue);
+      expect(arguments['feature'], equals('subscriptions'));
+    });
+  });
 }
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 36958d2..0ef17e7 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
@@ -141,4 +141,34 @@
       });
     });
   });
+
+  group('isFeatureSupported', () {
+    const String isFeatureSupportedMethodName =
+        'BillingClient#isFeatureSupported(String)';
+    test('isFeatureSupported returns false', () async {
+      late Map<Object?, Object?> arguments;
+      stubPlatform.addResponse(
+        name: isFeatureSupportedMethodName,
+        value: false,
+        additionalStepBeforeReturn: (value) => arguments = value,
+      );
+      final bool isSupported = await iapAndroidPlatformAddition
+          .isFeatureSupported(BillingClientFeature.subscriptions);
+      expect(isSupported, isFalse);
+      expect(arguments['feature'], equals('subscriptions'));
+    });
+
+    test('isFeatureSupported returns true', () async {
+      late Map<Object?, Object?> arguments;
+      stubPlatform.addResponse(
+        name: isFeatureSupportedMethodName,
+        value: true,
+        additionalStepBeforeReturn: (value) => arguments = value,
+      );
+      final bool isSupported = await iapAndroidPlatformAddition
+          .isFeatureSupported(BillingClientFeature.subscriptions);
+      expect(isSupported, isTrue);
+      expect(arguments['feature'], equals('subscriptions'));
+    });
+  });
 }