Added support to request list of purchases (#3944)

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 20c0274..949fe90 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.1
+
+* Added support to request a list of active subscriptions and non-consumed one-time purchases on Android, through the `InAppPurchaseAndroidPlatformAddition.queryPastPurchases` method.
+
 ## 0.1.0+1
 
 * Migrate maven repository from jcenter to mavenCentral.
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 e109c4e..84f8b9e 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
@@ -2,9 +2,12 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:flutter/services.dart';
+import 'package:in_app_purchase_android/in_app_purchase_android.dart';
 import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart';
 
 import '../billing_client_wrappers.dart';
+import 'types/types.dart';
 
 /// Contains InApp Purchase features that are only available on PlayStore.
 class InAppPurchaseAndroidPlatformAddition
@@ -52,4 +55,84 @@
     return _billingClient
         .consumeAsync(purchase.verificationData.serverVerificationData);
   }
+
+  /// Query all previous purchases.
+  ///
+  /// The `applicationUserName` should match whatever was sent in the initial
+  /// `PurchaseParam`, if anything. If no `applicationUserName` was specified in
+  /// the initial `PurchaseParam`, use `null`.
+  ///
+  /// This does not return consumed products. If you want to restore unused
+  /// consumable products, you need to persist consumable product information
+  /// for your user on your own server.
+  ///
+  /// See also:
+  ///
+  ///  * [refreshPurchaseVerificationData], for reloading failed
+  ///    [PurchaseDetails.verificationData].
+  Future<QueryPurchaseDetailsResponse> queryPastPurchases(
+      {String? applicationUserName}) async {
+    List<PurchasesResultWrapper> responses;
+    PlatformException? exception;
+    try {
+      responses = await Future.wait([
+        _billingClient.queryPurchases(SkuType.inapp),
+        _billingClient.queryPurchases(SkuType.subs)
+      ]);
+    } on PlatformException catch (e) {
+      exception = e;
+      responses = [
+        PurchasesResultWrapper(
+          responseCode: BillingResponse.error,
+          purchasesList: [],
+          billingResult: BillingResultWrapper(
+            responseCode: BillingResponse.error,
+            debugMessage: e.details.toString(),
+          ),
+        ),
+        PurchasesResultWrapper(
+          responseCode: BillingResponse.error,
+          purchasesList: [],
+          billingResult: BillingResultWrapper(
+            responseCode: BillingResponse.error,
+            debugMessage: e.details.toString(),
+          ),
+        )
+      ];
+    }
+
+    Set errorCodeSet = responses
+        .where((PurchasesResultWrapper response) =>
+            response.responseCode != BillingResponse.ok)
+        .map((PurchasesResultWrapper response) =>
+            response.responseCode.toString())
+        .toSet();
+
+    String errorMessage =
+        errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : '';
+
+    List<GooglePlayPurchaseDetails> pastPurchases =
+        responses.expand((PurchasesResultWrapper response) {
+      return response.purchasesList;
+    }).map((PurchaseWrapper purchaseWrapper) {
+      return GooglePlayPurchaseDetails.fromPurchase(purchaseWrapper);
+    }).toList();
+
+    IAPError? error;
+    if (exception != null) {
+      error = IAPError(
+          source: kIAPSource,
+          code: exception.code,
+          message: exception.message ?? '',
+          details: exception.details);
+    } else if (errorMessage.isNotEmpty) {
+      error = IAPError(
+          source: kIAPSource,
+          code: kRestoredPurchaseErrorCode,
+          message: errorMessage);
+    }
+
+    return QueryPurchaseDetailsResponse(
+        pastPurchases: pastPurchases, error: error);
+  }
 }
diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/query_purchase_details_response.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/query_purchase_details_response.dart
new file mode 100644
index 0000000..c0795a9
--- /dev/null
+++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/query_purchase_details_response.dart
@@ -0,0 +1,27 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart';
+
+import 'types.dart';
+
+/// The response object for fetching the past purchases.
+///
+/// An instance of this class is returned in [InAppPurchaseConnection.queryPastPurchases].
+class QueryPurchaseDetailsResponse {
+  /// Creates a new [QueryPurchaseDetailsResponse] object with the provider information.
+  QueryPurchaseDetailsResponse({required this.pastPurchases, this.error});
+
+  /// A list of successfully fetched past purchases.
+  ///
+  /// If there are no past purchases, or there is an [error] fetching past purchases,
+  /// this variable is an empty List.
+  /// You should verify the purchase data using [PurchaseDetails.verificationData] before using the [PurchaseDetails] object.
+  final List<GooglePlayPurchaseDetails> pastPurchases;
+
+  /// The error when fetching past purchases.
+  ///
+  /// If the fetch is successful, the value is `null`.
+  final IAPError? error;
+}
diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/types.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/types.dart
index 2982363..0a43425 100644
--- a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/types.dart
+++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/types.dart
@@ -6,3 +6,4 @@
 export 'google_play_product_details.dart';
 export 'google_play_purchase_details.dart';
 export 'google_play_purchase_param.dart';
+export 'query_purchase_details_response.dart';
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 cfb300d..4e78874 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.0+1
+version: 0.1.1
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
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 90b7154..36958d2 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
@@ -2,10 +2,12 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:flutter/services.dart';
 import 'package:flutter/widgets.dart' as widgets;
 import 'package:flutter_test/flutter_test.dart';
 import 'package:in_app_purchase_android/billing_client_wrappers.dart';
 import 'package:in_app_purchase_android/in_app_purchase_android.dart';
+import 'package:in_app_purchase_android/src/billing_client_wrappers/enum_converters.dart';
 import 'package:in_app_purchase_android/src/channel.dart';
 import 'package:in_app_purchase_android/src/in_app_purchase_android_platform_addition.dart';
 
@@ -61,4 +63,82 @@
       expect(billingResultWrapper, equals(expectedBillingResult));
     });
   });
+
+  group('queryPastPurchase', () {
+    group('queryPurchaseDetails', () {
+      const String queryMethodName = 'BillingClient#queryPurchases(String)';
+      test('handles error', () async {
+        const String debugMessage = 'dummy message';
+        final BillingResponse responseCode = BillingResponse.developerError;
+        final BillingResultWrapper expectedBillingResult = BillingResultWrapper(
+            responseCode: responseCode, debugMessage: debugMessage);
+
+        stubPlatform
+            .addResponse(name: queryMethodName, value: <dynamic, dynamic>{
+          'billingResult': buildBillingResultMap(expectedBillingResult),
+          'responseCode': BillingResponseConverter().toJson(responseCode),
+          'purchasesList': <Map<String, dynamic>>[]
+        });
+        final QueryPurchaseDetailsResponse response =
+            await iapAndroidPlatformAddition.queryPastPurchases();
+        expect(response.pastPurchases, isEmpty);
+        expect(response.error, isNotNull);
+        expect(
+            response.error!.message, BillingResponse.developerError.toString());
+        expect(response.error!.source, kIAPSource);
+      });
+
+      test('returns SkuDetailsResponseWrapper', () async {
+        const String debugMessage = 'dummy message';
+        final BillingResponse responseCode = BillingResponse.ok;
+        final BillingResultWrapper expectedBillingResult = BillingResultWrapper(
+            responseCode: responseCode, debugMessage: debugMessage);
+
+        stubPlatform
+            .addResponse(name: queryMethodName, value: <String, dynamic>{
+          'billingResult': buildBillingResultMap(expectedBillingResult),
+          'responseCode': BillingResponseConverter().toJson(responseCode),
+          'purchasesList': <Map<String, dynamic>>[
+            buildPurchaseMap(dummyPurchase),
+          ]
+        });
+
+        // Since queryPastPurchases makes 2 platform method calls (one for each SkuType), the result will contain 2 dummyWrapper instead
+        // of 1.
+        final QueryPurchaseDetailsResponse response =
+            await iapAndroidPlatformAddition.queryPastPurchases();
+        expect(response.error, isNull);
+        expect(response.pastPurchases.first.purchaseID, dummyPurchase.orderId);
+      });
+
+      test('should store platform exception in the response', () async {
+        const String debugMessage = 'dummy message';
+
+        final BillingResponse responseCode = BillingResponse.developerError;
+        final BillingResultWrapper expectedBillingResult = BillingResultWrapper(
+            responseCode: responseCode, debugMessage: debugMessage);
+        stubPlatform.addResponse(
+            name: queryMethodName,
+            value: <dynamic, dynamic>{
+              'responseCode': BillingResponseConverter().toJson(responseCode),
+              'billingResult': buildBillingResultMap(expectedBillingResult),
+              'purchasesList': <Map<String, dynamic>>[]
+            },
+            additionalStepBeforeReturn: (_) {
+              throw PlatformException(
+                code: 'error_code',
+                message: 'error_message',
+                details: {'info': 'error_info'},
+              );
+            });
+        final QueryPurchaseDetailsResponse response =
+            await iapAndroidPlatformAddition.queryPastPurchases();
+        expect(response.pastPurchases, isEmpty);
+        expect(response.error, isNotNull);
+        expect(response.error!.code, 'error_code');
+        expect(response.error!.message, 'error_message');
+        expect(response.error!.details, {'info': 'error_info'});
+      });
+    });
+  });
 }