From bf467c0788569602396d5518ba6c0940d44db1f0 Mon Sep 17 00:00:00 2001 From: "jakub.walusiak" Date: Sun, 26 Feb 2023 12:11:47 +0100 Subject: [PATCH 1/7] Recreating PR from flutter/plugins --- .../in_app_purchase_android/CHANGELOG.md | 5 + .../lib/billing_client_wrappers.dart | 1 + .../billing_client_manager.dart | 156 ++++++++++++++++++ .../billing_client_wrapper.dart | 6 + .../purchase_wrapper.dart | 9 +- .../sku_details_wrapper.dart | 9 +- .../src/in_app_purchase_android_platform.dart | 117 ++++++------- ...pp_purchase_android_platform_addition.dart | 28 +++- .../in_app_purchase_android/pubspec.yaml | 2 +- .../billing_client_manager_test.dart | 105 ++++++++++++ ...rchase_android_platform_addition_test.dart | 2 +- ...in_app_purchase_android_platform_test.dart | 57 ++++++- .../test/stub_in_app_purchase_platform.dart | 6 +- 13 files changed, 423 insertions(+), 80 deletions(-) create mode 100644 packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart create mode 100644 packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart 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 76c94cbab35..b8fecc8f7e6 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,8 @@ +## 0.2.5 + +* Fixes the management of `BillingClient` connection. +* Introduces `BillingClientManager`. + ## 0.2.4+1 * Updates Google Play Billing Library to 5.1.0. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/billing_client_wrappers.dart b/packages/in_app_purchase/in_app_purchase_android/lib/billing_client_wrappers.dart index 1dac19f825b..b49be8fe0fe 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/billing_client_wrappers.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/billing_client_wrappers.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'src/billing_client_wrappers/billing_client_manager.dart'; export 'src/billing_client_wrappers/billing_client_wrapper.dart'; export 'src/billing_client_wrappers/purchase_wrapper.dart'; export 'src/billing_client_wrappers/sku_details_wrapper.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart new file mode 100644 index 00000000000..31598621da0 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart @@ -0,0 +1,156 @@ +// 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 'dart:async'; + +import 'package:flutter/widgets.dart'; + +import 'billing_client_wrapper.dart'; +import 'purchase_wrapper.dart'; + +/// Abstraction of result of [BillingClient] operation that includes +/// a [BillingResponse]. +abstract class HasBillingResponse { + /// The status of the operation. + abstract final BillingResponse responseCode; +} + +/// Utility class that manages a [BillingClient] connection. +/// +/// Connection is initialized on creation of [BillingClientManager]. +/// If [BillingClient] sends `onBillingServiceDisconnected` event or any +/// operation returns [BillingResponse.serviceDisconnected], connection is +/// re-initialized. +/// +/// [BillingClient] instance is not exposed directly. It can be accessed via +/// [run] and [runRaw] methods that handle the connection management. +/// +/// Consider calling [dispose] after the [BillingClient] is no longer needed. +class BillingClientManager { + /// Creates the [BillingClientManager]. + /// + /// Immediately initializes connection to the underlying [BillingClient]. + BillingClientManager() { + _connect(); + } + + /// Stream of `onPurchasesUpdated` events from the [BillingClient]. + /// + /// This is a broadcast stream, so it can be listened to multiple times. + /// A "done" event will be sent after [dispose] is called. + late final Stream purchasesUpdatedStream = + _purchasesUpdatedController.stream; + + /// [BillingClient] instance managed by this [BillingClientManager]. + /// + /// In order to access the [BillingClient], consider using [run] and [runRaw] + /// methods. + @visibleForTesting + late final BillingClient client = BillingClient(_onPurchasesUpdated); + + final StreamController _purchasesUpdatedController = + StreamController.broadcast(); + + bool _isConnecting = false; + bool _isDisposed = false; + + // Initialized immediately in the constructor, so it's always safe to access. + late Future _readyFuture; + + /// Executes the given [block] with access to the underlying [BillingClient]. + /// + /// If necessary, waits for the underlying [BillingClient] to connect. + /// If given [block] returns [BillingResponse.serviceDisconnected], it will + /// be transparently retried after the connection is restored. Because + /// of this, [block] may be called multiple times. + /// + /// A response with [BillingResponse.serviceDisconnected] may be returned + /// in case of [dispose] being called during the operation. + /// + /// See [runRaw] for operations that do not return a subclass + /// of [HasBillingResponse]. + Future run( + Future Function(BillingClient client) block, + ) async { + assert(_debugAssertNotDisposed()); + await _readyFuture; + final R result = await block(client); + if (result.responseCode == BillingResponse.serviceDisconnected && + !_isDisposed) { + await _connect(); + return run(block); + } else { + return result; + } + } + + /// Executes the given [block] with access to the underlying [BillingClient]. + /// + /// If necessary, waits for the underlying [BillingClient] to connect. + /// Designed only for operations that do not return a subclass + /// of [HasBillingResponse] (e.g. [BillingClient.isReady], + /// [BillingClient.isFeatureSupported]). + /// + /// See [runRaw] for operations that return a subclass + /// of [HasBillingResponse]. + Future runRaw(Future Function(BillingClient client) block) async { + assert(_debugAssertNotDisposed()); + await _readyFuture; + return block(client); + } + + /// Ends connection to the [BillingClient]. + /// + /// Consider calling [dispose] after you no longer need the [BillingClient] + /// API to free up the resources. + /// + /// After calling [dispose] : + /// - Further connection attempts will not be made; + /// - [purchasesUpdatedStream] will be closed; + /// - Calls to [run] and [runRaw] will throw. + void dispose() { + assert(_debugAssertNotDisposed()); + _isDisposed = true; + client.endConnection(); + _purchasesUpdatedController.close(); + } + + // If disposed, does nothing. + // If currently connecting, waits for it to complete. + // Otherwise, starts a new connection. + Future _connect() { + if (_isDisposed) { + return Future.value(); + } + if (_isConnecting) { + return _readyFuture; + } + _isConnecting = true; + _readyFuture = Future.sync(() async { + await client.startConnection(onBillingServiceDisconnected: _connect); + _isConnecting = false; + }); + return _readyFuture; + } + + void _onPurchasesUpdated(PurchasesResultWrapper event) { + if (_isDisposed) { + return; + } + _purchasesUpdatedController.add(event); + } + + bool _debugAssertNotDisposed() { + assert(() { + if (_isDisposed) { + throw FlutterError( + 'A BillingClientManager was used after being disposed.\n' + 'Once you have called dispose() on a BillingClientManager, it can no longer be used.', + ); + } + return true; + }()); + return true; + } +} 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 2d4a3f96b50..04a73f6c564 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 @@ -51,6 +51,12 @@ typedef PurchasesUpdatedListener = void Function( /// `com.android.billingclient.api.BillingClient` API as much as possible, with /// some minor changes to account for language differences. Callbacks have been /// converted to futures where appropriate. +/// +/// Connection to [BillingClient] may be lost at any time (see +/// `onBillingServiceDisconnected` param of [startConnection] and +/// [BillingResponse.serviceDisconnected]). +/// Consider using [BillingClientManager] that handles these disconnections +/// transparently. class BillingClient { /// Creates a billing client. BillingClient(PurchasesUpdatedListener onPurchasesUpdated) { diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart index 4e6b953096e..633aa732165 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'billing_client_manager.dart'; import 'billing_client_wrapper.dart'; import 'sku_details_wrapper.dart'; @@ -265,7 +266,7 @@ class PurchaseHistoryRecordWrapper { @JsonSerializable() @BillingResponseConverter() @immutable -class PurchasesResultWrapper { +class PurchasesResultWrapper implements HasBillingResponse { /// Creates a [PurchasesResultWrapper] with the given purchase result details. const PurchasesResultWrapper( {required this.responseCode, @@ -300,6 +301,7 @@ class PurchasesResultWrapper { /// /// This can represent either the status of the "query purchase history" half /// of the operation and the "user made purchases" transaction itself. + @override final BillingResponse responseCode; /// The list of successful purchases made in this transaction. @@ -316,7 +318,7 @@ class PurchasesResultWrapper { @JsonSerializable() @BillingResponseConverter() @immutable -class PurchasesHistoryResult { +class PurchasesHistoryResult implements HasBillingResponse { /// Creates a [PurchasesHistoryResult] with the provided history. const PurchasesHistoryResult( {required this.billingResult, required this.purchaseHistoryRecordList}); @@ -325,6 +327,9 @@ class PurchasesHistoryResult { factory PurchasesHistoryResult.fromJson(Map map) => _$PurchasesHistoryResultFromJson(map); + @override + BillingResponse get responseCode => billingResult.responseCode; + @override bool operator ==(Object other) { if (identical(other, this)) { diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart index 1c5c2d1fcee..2689cf37eac 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'billing_client_manager.dart'; import 'billing_client_wrapper.dart'; // WARNING: Changes to `@JsonSerializable` classes need to be reflected in the @@ -182,7 +183,7 @@ class SkuDetailsWrapper { /// Returned by [BillingClient.querySkuDetails]. @JsonSerializable() @immutable -class SkuDetailsResponseWrapper { +class SkuDetailsResponseWrapper implements HasBillingResponse { /// Creates a [SkuDetailsResponseWrapper] with the given purchase details. @visibleForTesting const SkuDetailsResponseWrapper( @@ -202,6 +203,9 @@ class SkuDetailsResponseWrapper { @JsonKey(defaultValue: []) final List skuDetailsList; + @override + BillingResponse get responseCode => billingResult.responseCode; + @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) { @@ -221,7 +225,7 @@ class SkuDetailsResponseWrapper { @JsonSerializable() @BillingResponseConverter() @immutable -class BillingResultWrapper { +class BillingResultWrapper implements HasBillingResponse { /// Constructs the object with [responseCode] and [debugMessage]. const BillingResultWrapper({required this.responseCode, this.debugMessage}); @@ -239,6 +243,7 @@ class BillingResultWrapper { } /// Response code returned in the Play Billing API calls. + @override final BillingResponse responseCode; /// Debug message returned in the Play Billing API calls. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart index c8046d6e655..0dea21a579d 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart @@ -29,18 +29,13 @@ const String kIAPSource = 'google_play'; /// generic plugin API. class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { InAppPurchaseAndroidPlatform._() { - billingClient = BillingClient((PurchasesResultWrapper resultWrapper) async { - _purchaseUpdatedController - .add(await _getPurchaseDetailsFromResult(resultWrapper)); - }); - // Register [InAppPurchaseAndroidPlatformAddition]. InAppPurchasePlatformAddition.instance = - InAppPurchaseAndroidPlatformAddition(billingClient); + InAppPurchaseAndroidPlatformAddition(billingClientManager); - _readyFuture = _connect(); - _purchaseUpdatedController = - StreamController>.broadcast(); + billingClientManager.purchasesUpdatedStream + .asyncMap(_getPurchaseDetailsFromResult) + .listen(_purchaseUpdatedController.add); } /// Registers this class as the default instance of [InAppPurchasePlatform]. @@ -50,26 +45,25 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { InAppPurchasePlatform.instance = InAppPurchaseAndroidPlatform._(); } - static late StreamController> - _purchaseUpdatedController; + final StreamController> _purchaseUpdatedController = + StreamController>.broadcast(); @override - Stream> get purchaseStream => + late final Stream> purchaseStream = _purchaseUpdatedController.stream; /// The [BillingClient] that's abstracted by [GooglePlayConnection]. /// /// This field should not be used out of test code. @visibleForTesting - late final BillingClient billingClient; + final BillingClientManager billingClientManager = BillingClientManager(); - late Future _readyFuture; static final Set _productIdsToConsume = {}; @override Future isAvailable() async { - await _readyFuture; - return billingClient.isReady(); + return billingClientManager + .runRaw((BillingClient client) => client.isReady()); } @override @@ -77,27 +71,32 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { Set identifiers) async { List responses; PlatformException? exception; + + Future querySkuDetails(SkuType type) { + return billingClientManager.run( + (BillingClient client) => client.querySkuDetails( + skuType: type, + skusList: identifiers.toList(), + ), + ); + } + try { responses = await Future.wait(>[ - billingClient.querySkuDetails( - skuType: SkuType.inapp, skusList: identifiers.toList()), - billingClient.querySkuDetails( - skuType: SkuType.subs, skusList: identifiers.toList()) + querySkuDetails(SkuType.inapp), + querySkuDetails(SkuType.subs), ]); } on PlatformException catch (e) { exception = e; - responses = [ - // ignore: invalid_use_of_visible_for_testing_member - SkuDetailsResponseWrapper( - billingResult: BillingResultWrapper( - responseCode: BillingResponse.error, debugMessage: e.code), - skuDetailsList: const []), - // ignore: invalid_use_of_visible_for_testing_member - SkuDetailsResponseWrapper( - billingResult: BillingResultWrapper( - responseCode: BillingResponse.error, debugMessage: e.code), - skuDetailsList: const []) - ]; + // ignore: invalid_use_of_visible_for_testing_member + final SkuDetailsResponseWrapper response = SkuDetailsResponseWrapper( + billingResult: BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: e.code, + ), + skuDetailsList: const [], + ); + responses = [response, response]; } final List productDetailsList = responses.expand((SkuDetailsResponseWrapper response) { @@ -132,13 +131,16 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { } final BillingResultWrapper billingResultWrapper = - await billingClient.launchBillingFlow( - sku: purchaseParam.productDetails.id, - accountId: purchaseParam.applicationUserName, - oldSku: changeSubscriptionParam?.oldPurchaseDetails.productID, - purchaseToken: changeSubscriptionParam - ?.oldPurchaseDetails.verificationData.serverVerificationData, - prorationMode: changeSubscriptionParam?.prorationMode); + await billingClientManager.run( + (BillingClient client) => client.launchBillingFlow( + sku: purchaseParam.productDetails.id, + accountId: purchaseParam.applicationUserName, + oldSku: changeSubscriptionParam?.oldPurchaseDetails.productID, + purchaseToken: changeSubscriptionParam + ?.oldPurchaseDetails.verificationData.serverVerificationData, + prorationMode: changeSubscriptionParam?.prorationMode, + ), + ); return billingResultWrapper.responseCode == BillingResponse.ok; } @@ -171,8 +173,10 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { 'completePurchase unsuccessful. The `purchase.verificationData` is not valid'); } - return billingClient - .acknowledgePurchase(purchase.verificationData.serverVerificationData); + return billingClientManager.run( + (BillingClient client) => client.acknowledgePurchase( + purchase.verificationData.serverVerificationData), + ); } @override @@ -182,8 +186,10 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { List responses; responses = await Future.wait(>[ - billingClient.queryPurchases(SkuType.inapp), - billingClient.queryPurchases(SkuType.subs) + billingClientManager + .run((BillingClient client) => client.queryPurchases(SkuType.inapp)), + billingClientManager + .run((BillingClient client) => client.queryPurchases(SkuType.subs)), ]); final Set errorCodeSet = responses @@ -219,11 +225,9 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { _purchaseUpdatedController.add(pastPurchases); } - Future _connect() => - billingClient.startConnection(onBillingServiceDisconnected: () {}); - Future _maybeAutoConsumePurchase( - PurchaseDetails purchaseDetails) async { + PurchaseDetails purchaseDetails, + ) async { if (!(purchaseDetails.status == PurchaseStatus.purchased && _productIdsToConsume.contains(purchaseDetails.productID))) { return purchaseDetails; @@ -279,15 +283,16 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { } return [ PurchaseDetails( - purchaseID: '', - productID: '', - status: status, - transactionDate: null, - verificationData: PurchaseVerificationData( - localVerificationData: '', - serverVerificationData: '', - source: kIAPSource)) - ..error = error + purchaseID: '', + productID: '', + status: status, + transactionDate: null, + verificationData: PurchaseVerificationData( + localVerificationData: '', + serverVerificationData: '', + source: kIAPSource, + ), + )..error = error ]; } } 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 d5657d1a38d..d5bfc73868a 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 @@ -12,8 +12,8 @@ import '../in_app_purchase_android.dart'; class InAppPurchaseAndroidPlatformAddition extends InAppPurchasePlatformAddition { /// Creates a [InAppPurchaseAndroidPlatformAddition] which uses the supplied - /// `BillingClient` to provide Android specific features. - InAppPurchaseAndroidPlatformAddition(this._billingClient); + /// `BillingClientManager` to provide Android specific features. + InAppPurchaseAndroidPlatformAddition(this._billingClientManager); /// Whether pending purchase is enabled. /// @@ -42,7 +42,7 @@ class InAppPurchaseAndroidPlatformAddition // No-op, until it is time to completely remove this method from the API. } - final BillingClient _billingClient; + final BillingClientManager _billingClientManager; /// Mark that the user has consumed a product. /// @@ -54,8 +54,10 @@ class InAppPurchaseAndroidPlatformAddition throw ArgumentError( 'consumePurchase unsuccessful. The `purchase.verificationData` is not valid'); } - return _billingClient - .consumeAsync(purchase.verificationData.serverVerificationData); + return _billingClientManager.run( + (BillingClient client) => + client.consumeAsync(purchase.verificationData.serverVerificationData), + ); } /// Query all previous purchases. @@ -78,8 +80,12 @@ class InAppPurchaseAndroidPlatformAddition PlatformException? exception; try { responses = await Future.wait(>[ - _billingClient.queryPurchases(SkuType.inapp), - _billingClient.queryPurchases(SkuType.subs) + _billingClientManager.run( + (BillingClient client) => client.queryPurchases(SkuType.inapp), + ), + _billingClientManager.run( + (BillingClient client) => client.queryPurchases(SkuType.subs), + ), ]); } on PlatformException catch (e) { exception = e; @@ -141,7 +147,8 @@ class InAppPurchaseAndroidPlatformAddition /// 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 isFeatureSupported(BillingClientFeature feature) async { - return _billingClient.isFeatureSupported(feature); + return _billingClientManager + .runRaw((BillingClient client) => client.isFeatureSupported(feature)); } /// Initiates a flow to confirm the change of price for an item subscribed by the user. @@ -153,6 +160,9 @@ class InAppPurchaseAndroidPlatformAddition /// [InAppPurchaseAndroidPlatform.queryProductDetails] call. Future launchPriceChangeConfirmationFlow( {required String sku}) { - return _billingClient.launchPriceChangeConfirmationFlow(sku: sku); + return _billingClientManager.run( + (BillingClient client) => + client.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 397e82a8244..ffb55e947b3 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/main/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.2.4+1 +version: 0.2.5 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart new file mode 100644 index 00000000000..8351395cb46 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart @@ -0,0 +1,105 @@ +// 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 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_android/billing_client_wrappers.dart'; +import 'package:in_app_purchase_android/src/channel.dart'; + +import '../stub_in_app_purchase_platform.dart'; +import 'purchase_wrapper_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); + late BillingClientManager manager; + late Completer connectedCompleter; + + const String startConnectionCall = + 'BillingClient#startConnection(BillingClientStateListener)'; + const String endConnectionCall = 'BillingClient#endConnection()'; + const String onBillingServiceDisconnectedCallback = + 'BillingClientStateListener#onBillingServiceDisconnected()'; + + setUpAll(() => + channel.setMockMethodCallHandler(stubPlatform.fakeMethodCallHandler)); + + setUp(() { + WidgetsFlutterBinding.ensureInitialized(); + connectedCompleter = Completer.sync(); + stubPlatform.addResponse( + name: startConnectionCall, + value: buildBillingResultMap( + const BillingResultWrapper(responseCode: BillingResponse.ok), + ), + additionalStepBeforeReturn: (dynamic _) => connectedCompleter.future, + ); + stubPlatform.addResponse(name: endConnectionCall); + manager = BillingClientManager(); + }); + + tearDown(() => stubPlatform.reset()); + + group('BillingClientWrapper', () { + test('connects on initialization', () { + expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(1)); + }); + + test('waits for connection before executing the operations', () { + bool runCalled = false; + bool runRawCalled = false; + manager.run((BillingClient _) async { + runCalled = true; + return const BillingResultWrapper(responseCode: BillingResponse.ok); + }); + manager.runRaw((BillingClient _) async => runRawCalled = true); + expect(runCalled, equals(false)); + expect(runRawCalled, equals(false)); + connectedCompleter.complete(); + expect(runCalled, equals(true)); + expect(runRawCalled, equals(true)); + }); + + test('re-connects when client sends onBillingServiceDisconnected', () { + connectedCompleter.complete(); + manager.client.callHandler( + const MethodCall(onBillingServiceDisconnectedCallback, + {'handle': 0}), + ); + expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(2)); + }); + + test( + 're-connects when operation returns BillingResponse.serviceDisconnected', + () async { + connectedCompleter.complete(); + int timesCalled = 0; + final BillingResultWrapper result = await manager.run( + (BillingClient _) async { + timesCalled++; + return BillingResultWrapper( + responseCode: timesCalled == 1 + ? BillingResponse.serviceDisconnected + : BillingResponse.ok, + ); + }, + ); + expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(2)); + expect(timesCalled, equals(2)); + expect(result.responseCode, equals(BillingResponse.ok)); + }, + ); + + test('does not re-connect when disposed', () { + connectedCompleter.complete(); + manager.dispose(); + expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(1)); + expect(stubPlatform.countPreviousCalls(endConnectionCall), equals(1)); + }); + }); +} 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 a97c69608a3..1b61f53b0d3 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 @@ -39,7 +39,7 @@ void main() { value: buildBillingResultMap(expectedBillingResult)); stubPlatform.addResponse(name: endConnectionCall); iapAndroidPlatformAddition = - InAppPurchaseAndroidPlatformAddition(BillingClient((_) {})); + InAppPurchaseAndroidPlatformAddition(BillingClientManager()); }); group('consume purchases', () { diff --git a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart index 347bacde20b..a679def27d5 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart @@ -24,6 +24,10 @@ void main() { const String startConnectionCall = 'BillingClient#startConnection(BillingClientStateListener)'; const String endConnectionCall = 'BillingClient#endConnection()'; + const String acknowledgePurchaseCall = + 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; + const String onBillingServiceDisconnectedCallback = + 'BillingClientStateListener#onBillingServiceDisconnected()'; setUpAll(() { _ambiguate(TestDefaultBinaryMessengerBinding.instance)! @@ -57,6 +61,45 @@ void main() { //await iapAndroidPlatform.isAvailable(); expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(1)); }); + + test('re-connects when client sends onBillingServiceDisconnected', () { + iapAndroidPlatform.billingClientManager.client.callHandler( + const MethodCall(onBillingServiceDisconnectedCallback, + {'handle': 0}), + ); + expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(2)); + }); + + test( + 're-connects when operation returns BillingResponse.clientDisconnected', + () async { + final Map okValue = buildBillingResultMap( + const BillingResultWrapper(responseCode: BillingResponse.ok)); + stubPlatform.addResponse( + name: acknowledgePurchaseCall, + value: buildBillingResultMap( + const BillingResultWrapper( + responseCode: BillingResponse.serviceDisconnected, + ), + ), + ); + stubPlatform.addResponse( + name: startConnectionCall, + value: okValue, + additionalStepBeforeReturn: (dynamic _) => stubPlatform.addResponse( + name: acknowledgePurchaseCall, value: okValue), + ); + final PurchaseDetails purchase = + GooglePlayPurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase); + final BillingResultWrapper result = + await iapAndroidPlatform.completePurchase(purchase); + expect( + stubPlatform.countPreviousCalls(acknowledgePurchaseCall), + equals(2), + ); + expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(2)); + expect(result.responseCode, equals(BillingResponse.ok)); + }); }); group('isAvailable', () { @@ -314,7 +357,7 @@ void main() { } ] }); - iapAndroidPlatform.billingClient.callHandler(call); + iapAndroidPlatform.billingClientManager.client.callHandler(call); }); final Completer completer = Completer(); PurchaseDetails purchaseDetails; @@ -358,7 +401,7 @@ void main() { 'responseCode': const BillingResponseConverter().toJson(sentCode), 'purchasesList': const [] }); - iapAndroidPlatform.billingClient.callHandler(call); + iapAndroidPlatform.billingClientManager.client.callHandler(call); }); final Completer completer = Completer(); PurchaseDetails purchaseDetails; @@ -416,7 +459,7 @@ void main() { } ] }); - iapAndroidPlatform.billingClient.callHandler(call); + iapAndroidPlatform.billingClientManager.client.callHandler(call); }); final Completer consumeCompleter = Completer(); // adding call back for consume purchase @@ -531,7 +574,7 @@ void main() { } ] }); - iapAndroidPlatform.billingClient.callHandler(call); + iapAndroidPlatform.billingClientManager.client.callHandler(call); }); final Completer consumeCompleter = Completer(); // adding call back for consume purchase @@ -609,7 +652,7 @@ void main() { } ] }); - iapAndroidPlatform.billingClient.callHandler(call); + iapAndroidPlatform.billingClientManager.client.callHandler(call); }); final Completer consumeCompleter = Completer(); // adding call back for consume purchase @@ -675,7 +718,7 @@ void main() { } ] }); - iapAndroidPlatform.billingClient.callHandler(call); + iapAndroidPlatform.billingClientManager.client.callHandler(call); }); final Completer consumeCompleter = Completer(); // adding call back for consume purchase @@ -733,7 +776,7 @@ void main() { 'responseCode': const BillingResponseConverter().toJson(sentCode), 'purchasesList': const [] }); - iapAndroidPlatform.billingClient.callHandler(call); + iapAndroidPlatform.billingClientManager.client.callHandler(call); }); final Completer completer = Completer(); diff --git a/packages/in_app_purchase/in_app_purchase_android/test/stub_in_app_purchase_platform.dart b/packages/in_app_purchase/in_app_purchase_android/test/stub_in_app_purchase_platform.dart index 75972e644fa..35e2807bc3b 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/stub_in_app_purchase_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/stub_in_app_purchase_platform.dart @@ -5,7 +5,9 @@ import 'dart:async'; import 'package:flutter/services.dart'; -typedef AdditionalSteps = void Function(dynamic args); +// `FutureOr` instead of `FutureOr` to avoid +// "don't assign to void" warnings. +typedef AdditionalSteps = FutureOr Function(dynamic args); class StubInAppPurchasePlatform { final Map _expectedCalls = {}; @@ -36,7 +38,7 @@ class StubInAppPurchasePlatform { _previousCalls.add(call); if (_expectedCalls.containsKey(call.method)) { if (_additionalSteps[call.method] != null) { - _additionalSteps[call.method]!(call.arguments); + await _additionalSteps[call.method]!(call.arguments); } return Future.sync(() => _expectedCalls[call.method]); } else { From d41bc53569494ee2b5061beddd57e3db545fa6f2 Mon Sep 17 00:00:00 2001 From: "jakub.walusiak" Date: Mon, 6 Mar 2023 10:07:39 +0100 Subject: [PATCH 2/7] [in_app_purchases_android_platform] Changed method names in BillingClientManager --- .../billing_client_manager.dart | 61 +++++++++--------- .../src/in_app_purchase_android_platform.dart | 63 ++++++++++--------- ...pp_purchase_android_platform_addition.dart | 33 +++++----- .../billing_client_manager_test.dart | 26 ++++---- site-shared | 2 +- 5 files changed, 94 insertions(+), 91 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart index 31598621da0..455369017d4 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart @@ -24,7 +24,8 @@ abstract class HasBillingResponse { /// re-initialized. /// /// [BillingClient] instance is not exposed directly. It can be accessed via -/// [run] and [runRaw] methods that handle the connection management. +/// [withClient] and [withClientNonRetryable] methods that handle the +/// connection management. /// /// Consider calling [dispose] after the [BillingClient] is no longer needed. class BillingClientManager { @@ -44,13 +45,14 @@ class BillingClientManager { /// [BillingClient] instance managed by this [BillingClientManager]. /// - /// In order to access the [BillingClient], consider using [run] and [runRaw] + /// In order to access the [BillingClient], consider using [withClient] + /// and [withClientNonRetryable] /// methods. @visibleForTesting late final BillingClient client = BillingClient(_onPurchasesUpdated); final StreamController _purchasesUpdatedController = - StreamController.broadcast(); + StreamController.broadcast(); bool _isConnecting = false; bool _isDisposed = false; @@ -58,46 +60,48 @@ class BillingClientManager { // Initialized immediately in the constructor, so it's always safe to access. late Future _readyFuture; - /// Executes the given [block] with access to the underlying [BillingClient]. + /// Executes the given [action] with access to the underlying [BillingClient]. /// /// If necessary, waits for the underlying [BillingClient] to connect. - /// If given [block] returns [BillingResponse.serviceDisconnected], it will + /// If given [action] returns [BillingResponse.serviceDisconnected], it will /// be transparently retried after the connection is restored. Because - /// of this, [block] may be called multiple times. + /// of this, [action] may be called multiple times. /// /// A response with [BillingResponse.serviceDisconnected] may be returned /// in case of [dispose] being called during the operation. /// - /// See [runRaw] for operations that do not return a subclass + /// See [withClientNonRetryable] for operations that do not return a subclass /// of [HasBillingResponse]. - Future run( - Future Function(BillingClient client) block, - ) async { - assert(_debugAssertNotDisposed()); + Future withClient( + Future Function(BillingClient client) action, + ) async { + _debugAssertNotDisposed(); await _readyFuture; - final R result = await block(client); + final R result = await action(client); if (result.responseCode == BillingResponse.serviceDisconnected && !_isDisposed) { await _connect(); - return run(block); + return withClient(action); } else { return result; } } - /// Executes the given [block] with access to the underlying [BillingClient]. + /// Executes the given [action] with access to the underlying [BillingClient]. /// /// If necessary, waits for the underlying [BillingClient] to connect. /// Designed only for operations that do not return a subclass /// of [HasBillingResponse] (e.g. [BillingClient.isReady], /// [BillingClient.isFeatureSupported]). /// - /// See [runRaw] for operations that return a subclass + /// See [withClient] for operations that return a subclass /// of [HasBillingResponse]. - Future runRaw(Future Function(BillingClient client) block) async { - assert(_debugAssertNotDisposed()); + Future withClientNonRetryable( + Future Function(BillingClient client) action, + ) async { + _debugAssertNotDisposed(); await _readyFuture; - return block(client); + return action(client); } /// Ends connection to the [BillingClient]. @@ -108,9 +112,9 @@ class BillingClientManager { /// After calling [dispose] : /// - Further connection attempts will not be made; /// - [purchasesUpdatedStream] will be closed; - /// - Calls to [run] and [runRaw] will throw. + /// - Calls to [withClient] and [withClientNonRetryable] will throw. void dispose() { - assert(_debugAssertNotDisposed()); + _debugAssertNotDisposed(); _isDisposed = true; client.endConnection(); _purchasesUpdatedController.close(); @@ -141,16 +145,11 @@ class BillingClientManager { _purchasesUpdatedController.add(event); } - bool _debugAssertNotDisposed() { - assert(() { - if (_isDisposed) { - throw FlutterError( - 'A BillingClientManager was used after being disposed.\n' - 'Once you have called dispose() on a BillingClientManager, it can no longer be used.', - ); - } - return true; - }()); - return true; + void _debugAssertNotDisposed() { + assert( + !_isDisposed, + 'A BillingClientManager was used after being disposed. Once you have ' + 'called dispose() on a BillingClientManager, it can no longer be used.', + ); } } diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart index 0dea21a579d..9f224c99e2a 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart @@ -46,7 +46,7 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { } final StreamController> _purchaseUpdatedController = - StreamController>.broadcast(); + StreamController>.broadcast(); @override late final Stream> purchaseStream = @@ -63,7 +63,7 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { @override Future isAvailable() async { return billingClientManager - .runRaw((BillingClient client) => client.isReady()); + .withClientNonRetryable((BillingClient client) => client.isReady()); } @override @@ -73,8 +73,8 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { PlatformException? exception; Future querySkuDetails(SkuType type) { - return billingClientManager.run( - (BillingClient client) => client.querySkuDetails( + return billingClientManager.withClient( + (BillingClient client) => client.querySkuDetails( skuType: type, skusList: identifiers.toList(), ), @@ -96,10 +96,11 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { ), skuDetailsList: const [], ); + // Error response for both queries should be the same, so we can reuse it. responses = [response, response]; } final List productDetailsList = - responses.expand((SkuDetailsResponseWrapper response) { + responses.expand((SkuDetailsResponseWrapper response) { return response.skuDetailsList; }).map((SkuDetailsWrapper skuDetailWrapper) { return GooglePlayProductDetails.fromSkuDetails(skuDetailWrapper); @@ -109,17 +110,17 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { .map((ProductDetails productDetails) => productDetails.id) .toSet(); final List notFoundIDS = - identifiers.difference(successIDS).toList(); + identifiers.difference(successIDS).toList(); return ProductDetailsResponse( productDetails: productDetailsList, notFoundIDs: notFoundIDS, error: exception == null ? null : IAPError( - source: kIAPSource, - code: exception.code, - message: exception.message ?? '', - details: exception.details)); + source: kIAPSource, + code: exception.code, + message: exception.message ?? '', + details: exception.details)); } @override @@ -131,8 +132,8 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { } final BillingResultWrapper billingResultWrapper = - await billingClientManager.run( - (BillingClient client) => client.launchBillingFlow( + await billingClientManager.withClient( + (BillingClient client) => client.launchBillingFlow( sku: purchaseParam.productDetails.id, accountId: purchaseParam.applicationUserName, oldSku: changeSubscriptionParam?.oldPurchaseDetails.productID, @@ -157,12 +158,12 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { Future completePurchase( PurchaseDetails purchase) async { assert( - purchase is GooglePlayPurchaseDetails, - 'On Android, the `purchase` should always be of type `GooglePlayPurchaseDetails`.', + purchase is GooglePlayPurchaseDetails, + 'On Android, the `purchase` should always be of type `GooglePlayPurchaseDetails`.', ); final GooglePlayPurchaseDetails googlePurchase = - purchase as GooglePlayPurchaseDetails; + purchase as GooglePlayPurchaseDetails; if (googlePurchase.billingClientPurchase.isAcknowledged) { return const BillingResultWrapper(responseCode: BillingResponse.ok); @@ -173,8 +174,8 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { 'completePurchase unsuccessful. The `purchase.verificationData` is not valid'); } - return billingClientManager.run( - (BillingClient client) => client.acknowledgePurchase( + return billingClientManager.withClient( + (BillingClient client) => client.acknowledgePurchase( purchase.verificationData.serverVerificationData), ); } @@ -187,27 +188,27 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { responses = await Future.wait(>[ billingClientManager - .run((BillingClient client) => client.queryPurchases(SkuType.inapp)), + .withClient((BillingClient client) => client.queryPurchases(SkuType.inapp)), billingClientManager - .run((BillingClient client) => client.queryPurchases(SkuType.subs)), + .withClient((BillingClient client) => client.queryPurchases(SkuType.subs)), ]); final Set errorCodeSet = responses .where((PurchasesResultWrapper response) => - response.responseCode != BillingResponse.ok) + response.responseCode != BillingResponse.ok) .map((PurchasesResultWrapper response) => - response.responseCode.toString()) + response.responseCode.toString()) .toSet(); final String errorMessage = - errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : ''; + errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : ''; final List pastPurchases = - responses.expand((PurchasesResultWrapper response) { + responses.expand((PurchasesResultWrapper response) { return response.purchasesList; }).map((PurchaseWrapper purchaseWrapper) { final GooglePlayPurchaseDetails purchaseDetails = - GooglePlayPurchaseDetails.fromPurchase(purchaseWrapper); + GooglePlayPurchaseDetails.fromPurchase(purchaseWrapper); purchaseDetails.status = PurchaseStatus.restored; @@ -226,17 +227,17 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { } Future _maybeAutoConsumePurchase( - PurchaseDetails purchaseDetails, - ) async { + PurchaseDetails purchaseDetails, + ) async { if (!(purchaseDetails.status == PurchaseStatus.purchased && _productIdsToConsume.contains(purchaseDetails.productID))) { return purchaseDetails; } final BillingResultWrapper billingResult = - await (InAppPurchasePlatformAddition.instance! - as InAppPurchaseAndroidPlatformAddition) - .consumePurchase(purchaseDetails); + await (InAppPurchasePlatformAddition.instance! + as InAppPurchaseAndroidPlatformAddition) + .consumePurchase(purchaseDetails); final BillingResponse consumedResponse = billingResult.responseCode; if (consumedResponse != BillingResponse.ok) { purchaseDetails.status = PurchaseStatus.error; @@ -264,9 +265,9 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { ); } final List> purchases = - resultWrapper.purchasesList.map((PurchaseWrapper purchase) { + resultWrapper.purchasesList.map((PurchaseWrapper purchase) { final GooglePlayPurchaseDetails googlePlayPurchaseDetails = - GooglePlayPurchaseDetails.fromPurchase(purchase)..error = error; + GooglePlayPurchaseDetails.fromPurchase(purchase)..error = error; if (resultWrapper.responseCode == BillingResponse.userCanceled) { googlePlayPurchaseDetails.status = PurchaseStatus.canceled; } 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 d5bfc73868a..d8d297bda0b 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 @@ -26,8 +26,8 @@ class InAppPurchaseAndroidPlatformAddition /// See also [enablePendingPurchases] for more on pending purchases. @Deprecated( 'The requirement to call `enablePendingPurchases()` has become obsolete ' - "since Google Play no longer accepts app submissions that don't support " - 'pending purchases.') + "since Google Play no longer accepts app submissions that don't support " + 'pending purchases.') static bool get enablePendingPurchase => true; /// Enable the [InAppPurchaseConnection] to handle pending purchases. @@ -36,8 +36,8 @@ class InAppPurchaseAndroidPlatformAddition /// [enablePendingPurchases] when initializing your application. @Deprecated( 'The requirement to call `enablePendingPurchases()` has become obsolete ' - "since Google Play no longer accepts app submissions that don't support " - 'pending purchases.') + "since Google Play no longer accepts app submissions that don't support " + 'pending purchases.') static void enablePendingPurchases() { // No-op, until it is time to completely remove this method from the API. } @@ -54,7 +54,7 @@ class InAppPurchaseAndroidPlatformAddition throw ArgumentError( 'consumePurchase unsuccessful. The `purchase.verificationData` is not valid'); } - return _billingClientManager.run( + return _billingClientManager.withClient( (BillingClient client) => client.consumeAsync(purchase.verificationData.serverVerificationData), ); @@ -80,11 +80,11 @@ class InAppPurchaseAndroidPlatformAddition PlatformException? exception; try { responses = await Future.wait(>[ - _billingClientManager.run( - (BillingClient client) => client.queryPurchases(SkuType.inapp), + _billingClientManager.withClient( + (BillingClient client) => client.queryPurchases(SkuType.inapp), ), - _billingClientManager.run( - (BillingClient client) => client.queryPurchases(SkuType.subs), + _billingClientManager.withClient( + (BillingClient client) => client.queryPurchases(SkuType.subs), ), ]); } on PlatformException catch (e) { @@ -111,16 +111,16 @@ class InAppPurchaseAndroidPlatformAddition final Set errorCodeSet = responses .where((PurchasesResultWrapper response) => - response.responseCode != BillingResponse.ok) + response.responseCode != BillingResponse.ok) .map((PurchasesResultWrapper response) => - response.responseCode.toString()) + response.responseCode.toString()) .toSet(); final String errorMessage = - errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : ''; + errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : ''; final List pastPurchases = - responses.expand((PurchasesResultWrapper response) { + responses.expand((PurchasesResultWrapper response) { return response.purchasesList; }).map((PurchaseWrapper purchaseWrapper) { return GooglePlayPurchaseDetails.fromPurchase(purchaseWrapper); @@ -147,8 +147,9 @@ class InAppPurchaseAndroidPlatformAddition /// 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 isFeatureSupported(BillingClientFeature feature) async { - return _billingClientManager - .runRaw((BillingClient client) => client.isFeatureSupported(feature)); + return _billingClientManager.withClientNonRetryable( + (BillingClient client) => client.isFeatureSupported(feature), + ); } /// Initiates a flow to confirm the change of price for an item subscribed by the user. @@ -160,7 +161,7 @@ class InAppPurchaseAndroidPlatformAddition /// [InAppPurchaseAndroidPlatform.queryProductDetails] call. Future launchPriceChangeConfirmationFlow( {required String sku}) { - return _billingClientManager.run( + return _billingClientManager.withClient( (BillingClient client) => client.launchPriceChangeConfirmationFlow(sku: sku), ); diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart index 8351395cb46..43bde7200ce 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart @@ -51,18 +51,20 @@ void main() { }); test('waits for connection before executing the operations', () { - bool runCalled = false; - bool runRawCalled = false; - manager.run((BillingClient _) async { - runCalled = true; + bool called1 = false; + bool called2 = false; + manager.withClient((BillingClient _) async { + called1 = true; return const BillingResultWrapper(responseCode: BillingResponse.ok); }); - manager.runRaw((BillingClient _) async => runRawCalled = true); - expect(runCalled, equals(false)); - expect(runRawCalled, equals(false)); + manager.withClientNonRetryable( + (BillingClient _) async => called2 = true, + ); + expect(called1, equals(false)); + expect(called2, equals(false)); connectedCompleter.complete(); - expect(runCalled, equals(true)); - expect(runRawCalled, equals(true)); + expect(called1, equals(true)); + expect(called2, equals(true)); }); test('re-connects when client sends onBillingServiceDisconnected', () { @@ -76,11 +78,11 @@ void main() { test( 're-connects when operation returns BillingResponse.serviceDisconnected', - () async { + () async { connectedCompleter.complete(); int timesCalled = 0; - final BillingResultWrapper result = await manager.run( - (BillingClient _) async { + final BillingResultWrapper result = await manager.withClient( + (BillingClient _) async { timesCalled++; return BillingResultWrapper( responseCode: timesCalled == 1 diff --git a/site-shared b/site-shared index 8c92e5bdfdc..0e42e66c15e 160000 --- a/site-shared +++ b/site-shared @@ -1 +1 @@ -Subproject commit 8c92e5bdfdce14887605de6b5d9d0bd2615876b7 +Subproject commit 0e42e66c15eb439d513ecae1824aa6da0b3f5e76 From 3730512698842ae16070fa609b3e7fcabdc994ac Mon Sep 17 00:00:00 2001 From: "jakub.walusiak" Date: Tue, 7 Mar 2023 10:14:50 +0100 Subject: [PATCH 3/7] [in_app_purchases_android_platform] Changed method names in BillingClientManager --- .../billing_client_manager.dart | 20 +++++++++---------- .../src/in_app_purchase_android_platform.dart | 18 +++++++++-------- ...pp_purchase_android_platform_addition.dart | 10 +++++----- .../billing_client_manager_test.dart | 6 +++--- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart index 455369017d4..9a9d64891c2 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart @@ -24,7 +24,7 @@ abstract class HasBillingResponse { /// re-initialized. /// /// [BillingClient] instance is not exposed directly. It can be accessed via -/// [withClient] and [withClientNonRetryable] methods that handle the +/// [runWithClient] and [runWithClientNonRetryable] methods that handle the /// connection management. /// /// Consider calling [dispose] after the [BillingClient] is no longer needed. @@ -45,8 +45,8 @@ class BillingClientManager { /// [BillingClient] instance managed by this [BillingClientManager]. /// - /// In order to access the [BillingClient], consider using [withClient] - /// and [withClientNonRetryable] + /// In order to access the [BillingClient], consider using [runWithClient] + /// and [runWithClientNonRetryable] /// methods. @visibleForTesting late final BillingClient client = BillingClient(_onPurchasesUpdated); @@ -70,9 +70,9 @@ class BillingClientManager { /// A response with [BillingResponse.serviceDisconnected] may be returned /// in case of [dispose] being called during the operation. /// - /// See [withClientNonRetryable] for operations that do not return a subclass - /// of [HasBillingResponse]. - Future withClient( + /// See [runWithClientNonRetryable] for operations that do not return + /// a subclass of [HasBillingResponse]. + Future runWithClient( Future Function(BillingClient client) action, ) async { _debugAssertNotDisposed(); @@ -81,7 +81,7 @@ class BillingClientManager { if (result.responseCode == BillingResponse.serviceDisconnected && !_isDisposed) { await _connect(); - return withClient(action); + return runWithClient(action); } else { return result; } @@ -94,9 +94,9 @@ class BillingClientManager { /// of [HasBillingResponse] (e.g. [BillingClient.isReady], /// [BillingClient.isFeatureSupported]). /// - /// See [withClient] for operations that return a subclass + /// See [runWithClient] for operations that return a subclass /// of [HasBillingResponse]. - Future withClientNonRetryable( + Future runWithClientNonRetryable( Future Function(BillingClient client) action, ) async { _debugAssertNotDisposed(); @@ -112,7 +112,7 @@ class BillingClientManager { /// After calling [dispose] : /// - Further connection attempts will not be made; /// - [purchasesUpdatedStream] will be closed; - /// - Calls to [withClient] and [withClientNonRetryable] will throw. + /// - Calls to [runWithClient] and [runWithClientNonRetryable] will throw. void dispose() { _debugAssertNotDisposed(); _isDisposed = true; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart index 9f224c99e2a..8a1acc92209 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart @@ -63,7 +63,7 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { @override Future isAvailable() async { return billingClientManager - .withClientNonRetryable((BillingClient client) => client.isReady()); + .runWithClientNonRetryable((BillingClient client) => client.isReady()); } @override @@ -73,7 +73,7 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { PlatformException? exception; Future querySkuDetails(SkuType type) { - return billingClientManager.withClient( + return billingClientManager.runWithClient( (BillingClient client) => client.querySkuDetails( skuType: type, skusList: identifiers.toList(), @@ -132,7 +132,7 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { } final BillingResultWrapper billingResultWrapper = - await billingClientManager.withClient( + await billingClientManager.runWithClient( (BillingClient client) => client.launchBillingFlow( sku: purchaseParam.productDetails.id, accountId: purchaseParam.applicationUserName, @@ -174,7 +174,7 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { 'completePurchase unsuccessful. The `purchase.verificationData` is not valid'); } - return billingClientManager.withClient( + return billingClientManager.runWithClient( (BillingClient client) => client.acknowledgePurchase( purchase.verificationData.serverVerificationData), ); @@ -187,10 +187,12 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { List responses; responses = await Future.wait(>[ - billingClientManager - .withClient((BillingClient client) => client.queryPurchases(SkuType.inapp)), - billingClientManager - .withClient((BillingClient client) => client.queryPurchases(SkuType.subs)), + billingClientManager.runWithClient( + (BillingClient client) => client.queryPurchases(SkuType.inapp), + ), + billingClientManager.runWithClient( + (BillingClient client) => client.queryPurchases(SkuType.subs), + ), ]); final Set errorCodeSet = responses 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 d8d297bda0b..ef32ac2d4b8 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 @@ -54,7 +54,7 @@ class InAppPurchaseAndroidPlatformAddition throw ArgumentError( 'consumePurchase unsuccessful. The `purchase.verificationData` is not valid'); } - return _billingClientManager.withClient( + return _billingClientManager.runWithClient( (BillingClient client) => client.consumeAsync(purchase.verificationData.serverVerificationData), ); @@ -80,10 +80,10 @@ class InAppPurchaseAndroidPlatformAddition PlatformException? exception; try { responses = await Future.wait(>[ - _billingClientManager.withClient( + _billingClientManager.runWithClient( (BillingClient client) => client.queryPurchases(SkuType.inapp), ), - _billingClientManager.withClient( + _billingClientManager.runWithClient( (BillingClient client) => client.queryPurchases(SkuType.subs), ), ]); @@ -147,7 +147,7 @@ class InAppPurchaseAndroidPlatformAddition /// 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 isFeatureSupported(BillingClientFeature feature) async { - return _billingClientManager.withClientNonRetryable( + return _billingClientManager.runWithClientNonRetryable( (BillingClient client) => client.isFeatureSupported(feature), ); } @@ -161,7 +161,7 @@ class InAppPurchaseAndroidPlatformAddition /// [InAppPurchaseAndroidPlatform.queryProductDetails] call. Future launchPriceChangeConfirmationFlow( {required String sku}) { - return _billingClientManager.withClient( + return _billingClientManager.runWithClient( (BillingClient client) => client.launchPriceChangeConfirmationFlow(sku: sku), ); diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart index 43bde7200ce..fa354cff1da 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart @@ -53,11 +53,11 @@ void main() { test('waits for connection before executing the operations', () { bool called1 = false; bool called2 = false; - manager.withClient((BillingClient _) async { + manager.runWithClient((BillingClient _) async { called1 = true; return const BillingResultWrapper(responseCode: BillingResponse.ok); }); - manager.withClientNonRetryable( + manager.runWithClientNonRetryable( (BillingClient _) async => called2 = true, ); expect(called1, equals(false)); @@ -81,7 +81,7 @@ void main() { () async { connectedCompleter.complete(); int timesCalled = 0; - final BillingResultWrapper result = await manager.withClient( + final BillingResultWrapper result = await manager.runWithClient( (BillingClient _) async { timesCalled++; return BillingResultWrapper( From 7ad117aecb0d638e9336cc5cfc9b5b61dbbf62f7 Mon Sep 17 00:00:00 2001 From: "jakub.walusiak" Date: Sat, 11 Mar 2023 19:21:18 +0100 Subject: [PATCH 4/7] [in_app_purchases_android_platform] Reformat files, fix comments. --- .../billing_client_manager.dart | 27 +++++---- .../src/in_app_purchase_android_platform.dart | 56 +++++++++---------- ...pp_purchase_android_platform_addition.dart | 20 +++---- .../billing_client_manager_test.dart | 6 +- 4 files changed, 54 insertions(+), 55 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart index 9a9d64891c2..0eca29606a5 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart @@ -45,14 +45,13 @@ class BillingClientManager { /// [BillingClient] instance managed by this [BillingClientManager]. /// - /// In order to access the [BillingClient], consider using [runWithClient] - /// and [runWithClientNonRetryable] - /// methods. + /// In order to access the [BillingClient], use [runWithClient] + /// and [runWithClientNonRetryable] methods. @visibleForTesting late final BillingClient client = BillingClient(_onPurchasesUpdated); final StreamController _purchasesUpdatedController = - StreamController.broadcast(); + StreamController.broadcast(); bool _isConnecting = false; bool _isDisposed = false; @@ -73,8 +72,8 @@ class BillingClientManager { /// See [runWithClientNonRetryable] for operations that do not return /// a subclass of [HasBillingResponse]. Future runWithClient( - Future Function(BillingClient client) action, - ) async { + Future Function(BillingClient client) action, + ) async { _debugAssertNotDisposed(); await _readyFuture; final R result = await action(client); @@ -97,8 +96,8 @@ class BillingClientManager { /// See [runWithClient] for operations that return a subclass /// of [HasBillingResponse]. Future runWithClientNonRetryable( - Future Function(BillingClient client) action, - ) async { + Future Function(BillingClient client) action, + ) async { _debugAssertNotDisposed(); await _readyFuture; return action(client); @@ -109,9 +108,9 @@ class BillingClientManager { /// Consider calling [dispose] after you no longer need the [BillingClient] /// API to free up the resources. /// - /// After calling [dispose] : - /// - Further connection attempts will not be made; - /// - [purchasesUpdatedStream] will be closed; + /// After calling [dispose]: + /// - Further connection attempts will not be made. + /// - [purchasesUpdatedStream] will be closed. /// - Calls to [runWithClient] and [runWithClientNonRetryable] will throw. void dispose() { _debugAssertNotDisposed(); @@ -147,9 +146,9 @@ class BillingClientManager { void _debugAssertNotDisposed() { assert( - !_isDisposed, - 'A BillingClientManager was used after being disposed. Once you have ' - 'called dispose() on a BillingClientManager, it can no longer be used.', + !_isDisposed, + 'A BillingClientManager was used after being disposed. Once you have ' + 'called dispose() on a BillingClientManager, it can no longer be used.', ); } } diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart index 8a1acc92209..b524c203e9e 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart @@ -46,7 +46,7 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { } final StreamController> _purchaseUpdatedController = - StreamController>.broadcast(); + StreamController>.broadcast(); @override late final Stream> purchaseStream = @@ -74,7 +74,7 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { Future querySkuDetails(SkuType type) { return billingClientManager.runWithClient( - (BillingClient client) => client.querySkuDetails( + (BillingClient client) => client.querySkuDetails( skuType: type, skusList: identifiers.toList(), ), @@ -100,7 +100,7 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { responses = [response, response]; } final List productDetailsList = - responses.expand((SkuDetailsResponseWrapper response) { + responses.expand((SkuDetailsResponseWrapper response) { return response.skuDetailsList; }).map((SkuDetailsWrapper skuDetailWrapper) { return GooglePlayProductDetails.fromSkuDetails(skuDetailWrapper); @@ -110,17 +110,17 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { .map((ProductDetails productDetails) => productDetails.id) .toSet(); final List notFoundIDS = - identifiers.difference(successIDS).toList(); + identifiers.difference(successIDS).toList(); return ProductDetailsResponse( productDetails: productDetailsList, notFoundIDs: notFoundIDS, error: exception == null ? null : IAPError( - source: kIAPSource, - code: exception.code, - message: exception.message ?? '', - details: exception.details)); + source: kIAPSource, + code: exception.code, + message: exception.message ?? '', + details: exception.details)); } @override @@ -132,8 +132,8 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { } final BillingResultWrapper billingResultWrapper = - await billingClientManager.runWithClient( - (BillingClient client) => client.launchBillingFlow( + await billingClientManager.runWithClient( + (BillingClient client) => client.launchBillingFlow( sku: purchaseParam.productDetails.id, accountId: purchaseParam.applicationUserName, oldSku: changeSubscriptionParam?.oldPurchaseDetails.productID, @@ -158,12 +158,12 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { Future completePurchase( PurchaseDetails purchase) async { assert( - purchase is GooglePlayPurchaseDetails, - 'On Android, the `purchase` should always be of type `GooglePlayPurchaseDetails`.', + purchase is GooglePlayPurchaseDetails, + 'On Android, the `purchase` should always be of type `GooglePlayPurchaseDetails`.', ); final GooglePlayPurchaseDetails googlePurchase = - purchase as GooglePlayPurchaseDetails; + purchase as GooglePlayPurchaseDetails; if (googlePurchase.billingClientPurchase.isAcknowledged) { return const BillingResultWrapper(responseCode: BillingResponse.ok); @@ -175,7 +175,7 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { } return billingClientManager.runWithClient( - (BillingClient client) => client.acknowledgePurchase( + (BillingClient client) => client.acknowledgePurchase( purchase.verificationData.serverVerificationData), ); } @@ -188,29 +188,29 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { responses = await Future.wait(>[ billingClientManager.runWithClient( - (BillingClient client) => client.queryPurchases(SkuType.inapp), + (BillingClient client) => client.queryPurchases(SkuType.inapp), ), billingClientManager.runWithClient( - (BillingClient client) => client.queryPurchases(SkuType.subs), + (BillingClient client) => client.queryPurchases(SkuType.subs), ), ]); final Set errorCodeSet = responses .where((PurchasesResultWrapper response) => - response.responseCode != BillingResponse.ok) + response.responseCode != BillingResponse.ok) .map((PurchasesResultWrapper response) => - response.responseCode.toString()) + response.responseCode.toString()) .toSet(); final String errorMessage = - errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : ''; + errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : ''; final List pastPurchases = - responses.expand((PurchasesResultWrapper response) { + responses.expand((PurchasesResultWrapper response) { return response.purchasesList; }).map((PurchaseWrapper purchaseWrapper) { final GooglePlayPurchaseDetails purchaseDetails = - GooglePlayPurchaseDetails.fromPurchase(purchaseWrapper); + GooglePlayPurchaseDetails.fromPurchase(purchaseWrapper); purchaseDetails.status = PurchaseStatus.restored; @@ -229,17 +229,17 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { } Future _maybeAutoConsumePurchase( - PurchaseDetails purchaseDetails, - ) async { + PurchaseDetails purchaseDetails, + ) async { if (!(purchaseDetails.status == PurchaseStatus.purchased && _productIdsToConsume.contains(purchaseDetails.productID))) { return purchaseDetails; } final BillingResultWrapper billingResult = - await (InAppPurchasePlatformAddition.instance! - as InAppPurchaseAndroidPlatformAddition) - .consumePurchase(purchaseDetails); + await (InAppPurchasePlatformAddition.instance! + as InAppPurchaseAndroidPlatformAddition) + .consumePurchase(purchaseDetails); final BillingResponse consumedResponse = billingResult.responseCode; if (consumedResponse != BillingResponse.ok) { purchaseDetails.status = PurchaseStatus.error; @@ -267,9 +267,9 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { ); } final List> purchases = - resultWrapper.purchasesList.map((PurchaseWrapper purchase) { + resultWrapper.purchasesList.map((PurchaseWrapper purchase) { final GooglePlayPurchaseDetails googlePlayPurchaseDetails = - GooglePlayPurchaseDetails.fromPurchase(purchase)..error = error; + GooglePlayPurchaseDetails.fromPurchase(purchase)..error = error; if (resultWrapper.responseCode == BillingResponse.userCanceled) { googlePlayPurchaseDetails.status = PurchaseStatus.canceled; } 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 ef32ac2d4b8..67e21dfad8f 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 @@ -26,8 +26,8 @@ class InAppPurchaseAndroidPlatformAddition /// See also [enablePendingPurchases] for more on pending purchases. @Deprecated( 'The requirement to call `enablePendingPurchases()` has become obsolete ' - "since Google Play no longer accepts app submissions that don't support " - 'pending purchases.') + "since Google Play no longer accepts app submissions that don't support " + 'pending purchases.') static bool get enablePendingPurchase => true; /// Enable the [InAppPurchaseConnection] to handle pending purchases. @@ -36,8 +36,8 @@ class InAppPurchaseAndroidPlatformAddition /// [enablePendingPurchases] when initializing your application. @Deprecated( 'The requirement to call `enablePendingPurchases()` has become obsolete ' - "since Google Play no longer accepts app submissions that don't support " - 'pending purchases.') + "since Google Play no longer accepts app submissions that don't support " + 'pending purchases.') static void enablePendingPurchases() { // No-op, until it is time to completely remove this method from the API. } @@ -81,10 +81,10 @@ class InAppPurchaseAndroidPlatformAddition try { responses = await Future.wait(>[ _billingClientManager.runWithClient( - (BillingClient client) => client.queryPurchases(SkuType.inapp), + (BillingClient client) => client.queryPurchases(SkuType.inapp), ), _billingClientManager.runWithClient( - (BillingClient client) => client.queryPurchases(SkuType.subs), + (BillingClient client) => client.queryPurchases(SkuType.subs), ), ]); } on PlatformException catch (e) { @@ -111,16 +111,16 @@ class InAppPurchaseAndroidPlatformAddition final Set errorCodeSet = responses .where((PurchasesResultWrapper response) => - response.responseCode != BillingResponse.ok) + response.responseCode != BillingResponse.ok) .map((PurchasesResultWrapper response) => - response.responseCode.toString()) + response.responseCode.toString()) .toSet(); final String errorMessage = - errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : ''; + errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : ''; final List pastPurchases = - responses.expand((PurchasesResultWrapper response) { + responses.expand((PurchasesResultWrapper response) { return response.purchasesList; }).map((PurchaseWrapper purchaseWrapper) { return GooglePlayPurchaseDetails.fromPurchase(purchaseWrapper); diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart index fa354cff1da..9fd76f4aad6 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart @@ -58,7 +58,7 @@ void main() { return const BillingResultWrapper(responseCode: BillingResponse.ok); }); manager.runWithClientNonRetryable( - (BillingClient _) async => called2 = true, + (BillingClient _) async => called2 = true, ); expect(called1, equals(false)); expect(called2, equals(false)); @@ -78,11 +78,11 @@ void main() { test( 're-connects when operation returns BillingResponse.serviceDisconnected', - () async { + () async { connectedCompleter.complete(); int timesCalled = 0; final BillingResultWrapper result = await manager.runWithClient( - (BillingClient _) async { + (BillingClient _) async { timesCalled++; return BillingResultWrapper( responseCode: timesCalled == 1 From a525c4e35018b6f90df68775f83ecdf596eb0961 Mon Sep 17 00:00:00 2001 From: "jakub.walusiak" Date: Sat, 11 Mar 2023 19:25:27 +0100 Subject: [PATCH 5/7] [in_app_purchases_android_platform] Remove unnecessary refactor. --- .../lib/src/in_app_purchase_android_platform.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart index b524c203e9e..b605c2f611c 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart @@ -229,8 +229,7 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { } Future _maybeAutoConsumePurchase( - PurchaseDetails purchaseDetails, - ) async { + PurchaseDetails purchaseDetails) async { if (!(purchaseDetails.status == PurchaseStatus.purchased && _productIdsToConsume.contains(purchaseDetails.productID))) { return purchaseDetails; From 96b6eef1dc4cf98bf52fc546a46834f136ed5dc8 Mon Sep 17 00:00:00 2001 From: "jakub.walusiak" Date: Fri, 17 Mar 2023 10:10:57 +0100 Subject: [PATCH 6/7] [in_app_purchases_android_platform] Update CHANGELOG.md --- packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7a1019e10b5..4244c68aced 100644 --- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -1,6 +1,6 @@ ## 0.2.5 -* Fixes the management of `BillingClient` connection. +* Fixes the management of `BillingClient` connection by handling `BillingResponse.serviceDisconnected`. * Introduces `BillingClientManager`. ## 0.2.4+3 From 625758294eabb9e90844c4325108fd226cf0b53c Mon Sep 17 00:00:00 2001 From: "jakub.walusiak" Date: Fri, 17 Mar 2023 10:12:08 +0100 Subject: [PATCH 7/7] [in_app_purchases_android_platform] Update site-shared --- site-shared | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site-shared b/site-shared index 0e42e66c15e..8c92e5bdfdc 160000 --- a/site-shared +++ b/site-shared @@ -1 +1 @@ -Subproject commit 0e42e66c15eb439d513ecae1824aa6da0b3f5e76 +Subproject commit 8c92e5bdfdce14887605de6b5d9d0bd2615876b7