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 20c0274844b1..949fe907ab5d 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 e109c4e32ade..84f8b9ef1787 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 @@ class InAppPurchaseAndroidPlatformAddition 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 queryPastPurchases( + {String? applicationUserName}) async { + List 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 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 000000000000..c0795a9be573 --- /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 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 2982363c68ad..0a43425f6e94 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 'change_subscription_param.dart'; 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 cfb300d90588..4e78874cfc49 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 @@ name: in_app_purchase_android 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 90b7154257f7..36958d277f18 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 @@ void main() { 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: { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'purchasesList': >[] + }); + 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: { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'purchasesList': >[ + 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: { + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'purchasesList': >[] + }, + 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'}); + }); + }); + }); }