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