From f25101b2468609f63ec9adeed086cf1e82697620 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 29 Feb 2024 16:39:34 -0500 Subject: [PATCH 01/25] Convert one method end-to-end to get all the scaffolding in place --- .../inapppurchase/InAppPurchasePlugin.java | 6 +- .../plugins/inapppurchase/Messages.java | 93 +++++++++++++++++++ .../inapppurchase/MethodCallHandlerImpl.java | 27 +++--- .../inapppurchase/MethodCallHandlerTest.java | 24 ++--- .../billing_client_manager.dart | 22 ++++- .../billing_client_wrapper.dart | 12 ++- .../src/in_app_purchase_android_platform.dart | 13 ++- .../lib/src/messages.g.dart | 61 ++++++++++++ .../pigeons/copyright.txt | 3 + .../pigeons/messages.dart | 18 ++++ .../in_app_purchase_android/pubspec.yaml | 1 + .../billing_client_wrapper_test.dart | 13 ++- .../billing_client_wrapper_test.mocks.dart | 40 ++++++++ ...in_app_purchase_android_platform_test.dart | 24 ++++- 14 files changed, 314 insertions(+), 43 deletions(-) create mode 100644 packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java create mode 100644 packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart create mode 100644 packages/in_app_purchase/in_app_purchase_android/pigeons/copyright.txt create mode 100644 packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart create mode 100644 packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java index 4db2ca5d79e..a7872357cf1 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java @@ -45,7 +45,7 @@ public void onAttachedToEngine(@NonNull FlutterPlugin.FlutterPluginBinding bindi @Override public void onDetachedFromEngine(@NonNull FlutterPlugin.FlutterPluginBinding binding) { - teardownMethodChannel(); + teardownMethodChannel(binding.getBinaryMessenger()); } @Override @@ -76,9 +76,11 @@ private void setUpMethodChannel(BinaryMessenger messenger, Context context) { new MethodCallHandlerImpl( /*activity=*/ null, context, methodChannel, new BillingClientFactoryImpl()); methodChannel.setMethodCallHandler(methodCallHandler); + Messages.InAppPurchaseApi.setUp(messenger, methodCallHandler); } - private void teardownMethodChannel() { + private void teardownMethodChannel(BinaryMessenger messenger) { + Messages.InAppPurchaseApi.setUp(messenger, null); methodChannel.setMethodCallHandler(null); methodChannel = null; methodCallHandler = null; diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java new file mode 100644 index 00000000000..e5970955a00 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -0,0 +1,93 @@ +// 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. +// Autogenerated from Pigeon (v17.1.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +package io.flutter.plugins.inapppurchase; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MessageCodec; +import io.flutter.plugin.common.StandardMessageCodec; +import java.util.ArrayList; + +/** Generated class from Pigeon. */ +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) +public class Messages { + + /** Error class for passing custom error details to Flutter via a thrown PlatformException. */ + public static class FlutterError extends RuntimeException { + + /** The error code. */ + public final String code; + + /** The error details. Must be a datatype supported by the api codec. */ + public final Object details; + + public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) { + super(message); + this.code = code; + this.details = details; + } + } + + @NonNull + protected static ArrayList wrapError(@NonNull Throwable exception) { + ArrayList errorList = new ArrayList(3); + if (exception instanceof FlutterError) { + FlutterError error = (FlutterError) exception; + errorList.add(error.code); + errorList.add(error.getMessage()); + errorList.add(error.details); + } else { + errorList.add(exception.toString()); + errorList.add(exception.getClass().getSimpleName()); + errorList.add( + "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); + } + return errorList; + } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface InAppPurchaseApi { + /** Wraps BillingClient#isReady. */ + @NonNull + Boolean isReady(); + + /** The codec used by InAppPurchaseApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** + * Sets up an instance of `InAppPurchaseApi` to handle messages through the `binaryMessenger`. + */ + static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable InAppPurchaseApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.isReady", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + Boolean output = api.isReady(); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } +} diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index ecf88747779..dbb1584b115 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -4,6 +4,8 @@ package io.flutter.plugins.inapppurchase; +import static io.flutter.plugins.inapppurchase.Messages.FlutterError; +import static io.flutter.plugins.inapppurchase.Messages.InAppPurchaseApi; import static io.flutter.plugins.inapppurchase.Translator.fromAlternativeBillingOnlyReportingDetails; import static io.flutter.plugins.inapppurchase.Translator.fromBillingConfig; import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult; @@ -42,11 +44,12 @@ /** Handles method channel for the plugin. */ class MethodCallHandlerImpl - implements MethodChannel.MethodCallHandler, Application.ActivityLifecycleCallbacks { + implements MethodChannel.MethodCallHandler, + Application.ActivityLifecycleCallbacks, + InAppPurchaseApi { @VisibleForTesting static final class MethodNames { - static final String IS_READY = "BillingClient#isReady()"; static final String START_CONNECTION = "BillingClient#startConnection(BillingClientStateListener)"; static final String END_CONNECTION = "BillingClient#endConnection()"; @@ -171,9 +174,6 @@ void onDetachedFromActivity() { @Override public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { switch (call.method) { - case MethodNames.IS_READY: - isReady(result); - break; case MethodNames.START_CONNECTION: final int handle = (int) call.argument(MethodArgs.HANDLE); int billingChoiceMode = BillingChoiceMode.PLAY_BILLING_ONLY; @@ -297,12 +297,11 @@ private void endBillingClientConnection() { } } - private void isReady(MethodChannel.Result result) { - if (billingClientError(result)) { - return; - } - - result.success(billingClient.isReady()); + @Override + @NonNull + public Boolean isReady() { + validateBillingClient(); + return billingClient.isReady(); } private void queryProductDetailsAsync( @@ -566,6 +565,12 @@ private boolean billingClientError(MethodChannel.Result result) { return true; } + private void validateBillingClient() { + if (billingClient == null) { + throw new FlutterError("UNAVAILABLE", "BillingClient is unset. Try reconnecting.", null); + } + } + private void isFeatureSupported(String feature, MethodChannel.Result result) { if (billingClientError(result)) { return; diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index bfdf928ca51..fd2b52899f8 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -12,7 +12,6 @@ import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.GET_BILLING_CONFIG; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.IS_ALTERNATIVE_BILLING_ONLY_AVAILABLE; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.IS_FEATURE_SUPPORTED; -import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.IS_READY; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.LAUNCH_BILLING_FLOW; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.ON_DISCONNECT; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PRODUCT_DETAILS; @@ -32,6 +31,9 @@ import static java.util.Collections.unmodifiableList; import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.contains; @@ -131,31 +133,29 @@ public void invalidMethod() { @Test public void isReady_true() { mockStartConnection(); - MethodCall call = new MethodCall(IS_READY, null); when(mockBillingClient.isReady()).thenReturn(true); - methodChannelHandler.onMethodCall(call, result); - verify(result).success(true); + boolean result = methodChannelHandler.isReady(); + assertTrue(result); } @Test public void isReady_false() { mockStartConnection(); - MethodCall call = new MethodCall(IS_READY, null); when(mockBillingClient.isReady()).thenReturn(false); - methodChannelHandler.onMethodCall(call, result); - verify(result).success(false); + boolean result = methodChannelHandler.isReady(); + assertFalse(result); } @Test public void isReady_clientDisconnected() { MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); methodChannelHandler.onMethodCall(disconnectCall, mock(Result.class)); - MethodCall isReadyCall = new MethodCall(IS_READY, null); - methodChannelHandler.onMethodCall(isReadyCall, result); - - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); - verify(result, never()).success(any()); + Messages.FlutterError exception = + assertThrows( + Messages.FlutterError.class, + () -> methodChannelHandler.isReady()); + assertEquals("UNAVAILABLE", exception.code); } @Test 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 789ba5e01cc..85efbc62fd5 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 @@ -4,6 +4,7 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'billing_client_wrapper.dart'; @@ -16,6 +17,12 @@ abstract class HasBillingResponse { abstract final BillingResponse responseCode; } +/// Factory for creating BillingClient instances, to allow injection of +/// custom billing clients in tests. +@visibleForTesting +typedef BillingClientFactory = BillingClient Function( + PurchasesUpdatedListener onPurchasesUpdated); + /// Utility class that manages a [BillingClient] connection. /// /// Connection is initialized on creation of [BillingClientManager]. @@ -32,8 +39,10 @@ class BillingClientManager { /// Creates the [BillingClientManager]. /// /// Immediately initializes connection to the underlying [BillingClient]. - BillingClientManager() - : _billingChoiceMode = BillingChoiceMode.playBillingOnly { + BillingClientManager( + {@visibleForTesting BillingClientFactory? billingClientFactory}) + : _billingChoiceMode = BillingChoiceMode.playBillingOnly, + _billingClientFactory = billingClientFactory ?? _createBillingClient { _connect(); } @@ -49,12 +58,19 @@ class BillingClientManager { /// In order to access the [BillingClient], use [runWithClient] /// and [runWithClientNonRetryable] methods. @visibleForTesting - late final BillingClient client = BillingClient(_onPurchasesUpdated); + late final BillingClient client = _billingClientFactory(_onPurchasesUpdated); + + // Default (non-test) implementation of _billingClientFactory. + static BillingClient _createBillingClient( + PurchasesUpdatedListener onPurchasesUpdated) { + return BillingClient(onPurchasesUpdated); + } final StreamController _purchasesUpdatedController = StreamController.broadcast(); BillingChoiceMode _billingChoiceMode; + BillingClientFactory _billingClientFactory; bool _isConnecting = false; bool _isDisposed = false; 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 15dc4217fe6..c50374efb9e 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 @@ -10,6 +10,7 @@ import 'package:json_annotation/json_annotation.dart'; import '../../billing_client_wrappers.dart'; import '../channel.dart'; +import '../messages.g.dart'; import 'billing_config_wrapper.dart'; part 'billing_client_wrapper.g.dart'; @@ -60,13 +61,18 @@ typedef PurchasesUpdatedListener = void Function( /// transparently. class BillingClient { /// Creates a billing client. - BillingClient(PurchasesUpdatedListener onPurchasesUpdated) { + BillingClient( + PurchasesUpdatedListener onPurchasesUpdated, { + @visibleForTesting InAppPurchaseApi? api, + }) : _hostApi = api ?? InAppPurchaseApi() { channel.setMethodCallHandler(callHandler); _callbacks[kOnPurchasesUpdated] = [ onPurchasesUpdated ]; } + final InAppPurchaseApi _hostApi; + // Occasionally methods in the native layer require a Dart callback to be // triggered in response to a Java callback. For example, // [startConnection] registers an [OnBillingServiceDisconnected] callback. @@ -81,9 +87,7 @@ class BillingClient { /// [`BillingClient#isReady()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#isReady()) /// to get the ready status of the BillingClient instance. Future isReady() async { - final bool? ready = - await channel.invokeMethod('BillingClient#isReady()'); - return ready ?? false; + return _hostApi.isReady(); } /// Enable the [BillingClientWrapper] to handle pending purchases. 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 2e4ed5b0f92..56937c3aa57 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 @@ -4,12 +4,14 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import '../billing_client_wrappers.dart'; import '../in_app_purchase_android.dart'; +import 'billing_client_wrappers/billing_client_manager.dart'; /// [IAPError.code] code for failed purchases. const String kPurchaseErrorCode = 'purchase_error'; @@ -28,7 +30,12 @@ const String kIAPSource = 'google_play'; /// This translates various `BillingClient` calls and responses into the /// generic plugin API. class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { - InAppPurchaseAndroidPlatform._() { + /// Creates a new InAppPurchaseAndroidPlatform instance, and configures it + /// for use. + @visibleForTesting + InAppPurchaseAndroidPlatform( + {@visibleForTesting BillingClientManager? manager}) + : billingClientManager = manager ?? BillingClientManager() { // Register [InAppPurchaseAndroidPlatformAddition]. InAppPurchasePlatformAddition.instance = InAppPurchaseAndroidPlatformAddition(billingClientManager); @@ -42,7 +49,7 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { static void registerPlatform() { // Register the platform instance with the plugin platform // interface. - InAppPurchasePlatform.instance = InAppPurchaseAndroidPlatform._(); + InAppPurchasePlatform.instance = InAppPurchaseAndroidPlatform(); } final StreamController> _purchaseUpdatedController = @@ -56,7 +63,7 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { /// /// This field should not be used out of test code. @visibleForTesting - final BillingClientManager billingClientManager = BillingClientManager(); + final BillingClientManager billingClientManager; static final Set _productIdsToConsume = {}; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart new file mode 100644 index 00000000000..604148321a0 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -0,0 +1,61 @@ +// 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. +// Autogenerated from Pigeon (v17.1.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +class InAppPurchaseApi { + /// Constructor for [InAppPurchaseApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + InAppPurchaseApi({BinaryMessenger? binaryMessenger}) + : __pigeon_binaryMessenger = binaryMessenger; + final BinaryMessenger? __pigeon_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = + StandardMessageCodec(); + + /// Wraps BillingClient#isReady. + Future isReady() async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.isReady'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send(null) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as bool?)!; + } + } +} diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/copyright.txt b/packages/in_app_purchase/in_app_purchase_android/pigeons/copyright.txt new file mode 100644 index 00000000000..1236b63caf3 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/copyright.txt @@ -0,0 +1,3 @@ +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. diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart new file mode 100644 index 00000000000..203194905fa --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -0,0 +1,18 @@ +// 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:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + javaOptions: JavaOptions(package: 'io.flutter.plugins.inapppurchase'), + javaOut: + 'android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java', + copyrightHeader: 'pigeons/copyright.txt', +)) +@HostApi() +abstract class InAppPurchaseApi { + /// Wraps BillingClient#isReady. + bool isReady(); +} 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 7ce2e0080ce..317d2c1097b 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -29,6 +29,7 @@ dev_dependencies: sdk: flutter json_serializable: ^6.3.1 mockito: 5.4.4 + pigeon: ^17.1.1 test: ^1.16.0 topics: diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index 3ffcd8d5d08..22a415000b4 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -7,8 +7,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase_android/billing_client_wrappers.dart'; import 'package:in_app_purchase_android/src/billing_client_wrappers/billing_config_wrapper.dart'; import 'package:in_app_purchase_android/src/channel.dart'; +import 'package:in_app_purchase_android/src/messages.g.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; import '../stub_in_app_purchase_platform.dart'; +import 'billing_client_wrapper_test.mocks.dart'; import 'product_details_wrapper_test.dart'; import 'purchase_wrapper_test.dart'; @@ -26,10 +30,12 @@ const PurchaseWrapper dummyOldPurchase = PurchaseWrapper( purchaseState: PurchaseStateWrapper.purchased, ); +@GenerateMocks([InAppPurchaseApi]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); + late MockInAppPurchaseApi mockApi; late BillingClient billingClient; setUpAll(() => TestDefaultBinaryMessengerBinding @@ -37,18 +43,19 @@ void main() { .setMockMethodCallHandler(channel, stubPlatform.fakeMethodCallHandler)); setUp(() { - billingClient = BillingClient((PurchasesResultWrapper _) {}); + mockApi = MockInAppPurchaseApi(); + billingClient = BillingClient((PurchasesResultWrapper _) {}, api: mockApi); stubPlatform.reset(); }); group('isReady', () { test('true', () async { - stubPlatform.addResponse(name: 'BillingClient#isReady()', value: true); + when(mockApi.isReady()).thenAnswer((_) async => true); expect(await billingClient.isReady(), isTrue); }); test('false', () async { - stubPlatform.addResponse(name: 'BillingClient#isReady()', value: false); + when(mockApi.isReady()).thenAnswer((_) async => false); expect(await billingClient.isReady(), isFalse); }); }); diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart new file mode 100644 index 00000000000..48ff3c73c9c --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart @@ -0,0 +1,40 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:in_app_purchase_android/src/messages.g.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [InAppPurchaseApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { + MockInAppPurchaseApi() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future isReady() => (super.noSuchMethod( + Invocation.method( + #isReady, + [], + ), + returnValue: _i3.Future.value(false), + ) as _i3.Future); +} 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 b45efcf344f..b91585814fd 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 @@ -11,7 +11,9 @@ 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/channel.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; +import 'package:mockito/mockito.dart'; +import 'billing_client_wrappers/billing_client_wrapper_test.mocks.dart'; import 'billing_client_wrappers/product_details_wrapper_test.dart'; import 'billing_client_wrappers/purchase_wrapper_test.dart'; import 'stub_in_app_purchase_platform.dart'; @@ -20,6 +22,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); + late MockInAppPurchaseApi mockApi; late InAppPurchaseAndroidPlatform iapAndroidPlatform; const String startConnectionCall = 'BillingClient#startConnection(BillingClientStateListener)'; @@ -46,15 +49,26 @@ void main() { value: buildBillingResultMap(expectedBillingResult)); stubPlatform.addResponse(name: endConnectionCall); - InAppPurchaseAndroidPlatform.registerPlatform(); - iapAndroidPlatform = - InAppPurchasePlatform.instance as InAppPurchaseAndroidPlatform; + mockApi = MockInAppPurchaseApi(); + iapAndroidPlatform = InAppPurchaseAndroidPlatform( + manager: BillingClientManager( + billingClientFactory: (PurchasesUpdatedListener listener) => + BillingClient(listener, api: mockApi))); + InAppPurchasePlatform.instance = iapAndroidPlatform; }); tearDown(() { stubPlatform.reset(); }); + test('register sets an instance', () { + InAppPurchaseAndroidPlatform.registerPlatform(); + expect(InAppPurchasePlatform.instance, isA()); + // TODO(stuartmorgan): Refactor tests so that the instance isn't set by + // global test setup, so that this isn't necessary. + expect(InAppPurchasePlatform.instance, isNot(iapAndroidPlatform)); + }); + group('connection management', () { test('connects on initialization', () { //await iapAndroidPlatform.isAvailable(); @@ -104,12 +118,12 @@ void main() { group('isAvailable', () { test('true', () async { - stubPlatform.addResponse(name: 'BillingClient#isReady()', value: true); + when(mockApi.isReady()).thenAnswer((_) async => true); expect(await iapAndroidPlatform.isAvailable(), isTrue); }); test('false', () async { - stubPlatform.addResponse(name: 'BillingClient#isReady()', value: false); + when(mockApi.isReady()).thenAnswer((_) async => false); expect(await iapAndroidPlatform.isAvailable(), isFalse); }); }); From 2cc9af359d52fb235018745c458b4303734ecbdb Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 4 Mar 2024 13:59:10 -0500 Subject: [PATCH 02/25] Partially convert startConnection --- .../inapppurchase/BillingClientFactory.java | 5 +- .../BillingClientFactoryImpl.java | 8 +- .../plugins/inapppurchase/Messages.java | 85 +++++++++++ .../inapppurchase/MethodCallHandlerImpl.java | 40 +----- .../inapppurchase/MethodCallHandlerTest.java | 135 ++++++------------ .../billing_client_manager.dart | 2 +- .../billing_client_wrapper.dart | 14 +- .../lib/src/messages.g.dart | 42 ++++++ .../lib/src/pigeon_converters.dart | 16 +++ .../pigeons/messages.dart | 17 +++ .../billing_client_manager_test.dart | 56 ++++---- .../billing_client_wrapper_test.dart | 64 ++------- .../billing_client_wrapper_test.mocks.dart | 24 +++- ...rchase_android_platform_addition_test.dart | 51 +++---- ...in_app_purchase_android_platform_test.dart | 26 +--- 15 files changed, 313 insertions(+), 272 deletions(-) create mode 100644 packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java index 9324c9367ee..dbec5ed1a3c 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import com.android.billingclient.api.BillingClient; import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.inapppurchase.Messages.PlatformBillingChoiceMode; /** Responsible for creating a {@link BillingClient} object. */ interface BillingClientFactory { @@ -22,5 +23,7 @@ interface BillingClientFactory { * @return The {@link BillingClient} object that is created. */ BillingClient createBillingClient( - @NonNull Context context, @NonNull MethodChannel channel, int billingChoiceMode); + @NonNull Context context, + @NonNull MethodChannel channel, + PlatformBillingChoiceMode billingChoiceMode); } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java index c6911f8314a..3ab1eef5dfd 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java @@ -8,16 +8,18 @@ import androidx.annotation.NonNull; import com.android.billingclient.api.BillingClient; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.BillingChoiceMode; +import io.flutter.plugins.inapppurchase.Messages.PlatformBillingChoiceMode; /** The implementation for {@link BillingClientFactory} for the plugin. */ final class BillingClientFactoryImpl implements BillingClientFactory { @Override public BillingClient createBillingClient( - @NonNull Context context, @NonNull MethodChannel channel, int billingChoiceMode) { + @NonNull Context context, + @NonNull MethodChannel channel, + PlatformBillingChoiceMode billingChoiceMode) { BillingClient.Builder builder = BillingClient.newBuilder(context).enablePendingPurchases(); - if (billingChoiceMode == BillingChoiceMode.ALTERNATIVE_BILLING_ONLY) { + if (billingChoiceMode == PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY) { // https://developer.android.com/google/play/billing/alternative/alternative-billing-without-user-choice-in-app builder.enableAlternativeBillingOnly(); } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java index e5970955a00..0548fc7201f 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -14,6 +14,7 @@ import io.flutter.plugin.common.MessageCodec; import io.flutter.plugin.common.StandardMessageCodec; import java.util.ArrayList; +import java.util.Map; /** Generated class from Pigeon. */ @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) @@ -51,12 +52,60 @@ protected static ArrayList wrapError(@NonNull Throwable exception) { } return errorList; } + + /** Pigeon version of BillingChoiceMode. */ + public enum PlatformBillingChoiceMode { + /** + * Billing through google play. + * + *

Default state. + */ + PLAY_BILLING_ONLY(0), + /** Billing through app provided flow. */ + ALTERNATIVE_BILLING_ONLY(1); + + final int index; + + private PlatformBillingChoiceMode(final int index) { + this.index = index; + } + } + + /** Asynchronous error handling return type for non-nullable API method returns. */ + public interface Result { + /** Success case callback method for handling returns. */ + void success(@NonNull T result); + + /** Failure case callback method for handling errors. */ + void error(@NonNull Throwable error); + } + /** Asynchronous error handling return type for nullable API method returns. */ + public interface NullableResult { + /** Success case callback method for handling returns. */ + void success(@Nullable T result); + + /** Failure case callback method for handling errors. */ + void error(@NonNull Throwable error); + } + /** Asynchronous error handling return type for void API method returns. */ + public interface VoidResult { + /** Success case callback method for handling returns. */ + void success(); + + /** Failure case callback method for handling errors. */ + void error(@NonNull Throwable error); + } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface InAppPurchaseApi { /** Wraps BillingClient#isReady. */ @NonNull Boolean isReady(); + void startConnection( + @NonNull Long callbackHandle, + @NonNull PlatformBillingChoiceMode billingMode, + @NonNull Result> result); + /** The codec used by InAppPurchaseApi. */ static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); @@ -88,6 +137,42 @@ static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable InAppPurch channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.startConnection", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number callbackHandleArg = (Number) args.get(0); + PlatformBillingChoiceMode billingModeArg = + PlatformBillingChoiceMode.values()[(int) args.get(1)]; + Result> resultCallback = + new Result>() { + public void success(Map result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.startConnection( + (callbackHandleArg == null) ? null : callbackHandleArg.longValue(), + billingModeArg, + resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } } } } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index dbb1584b115..3a788a76830 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -50,8 +50,6 @@ class MethodCallHandlerImpl @VisibleForTesting static final class MethodNames { - static final String START_CONNECTION = - "BillingClient#startConnection(BillingClientStateListener)"; static final String END_CONNECTION = "BillingClient#endConnection()"; static final String ON_DISCONNECT = "BillingClientStateListener#onBillingServiceDisconnected()"; static final String QUERY_PRODUCT_DETAILS = @@ -79,26 +77,6 @@ static final class MethodNames { private MethodNames() {} } - @VisibleForTesting - static final class MethodArgs { - - // Key for an int argument passed into startConnection - static final String HANDLE = "handle"; - // Key for a boolean argument passed into startConnection. - static final String BILLING_CHOICE_MODE = "billingChoiceMode"; - - private MethodArgs() {} - } - - /** - * Values here must match values used in - * in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart - */ - static final class BillingChoiceMode { - static final int PLAY_BILLING_ONLY = 0; - static final int ALTERNATIVE_BILLING_ONLY = 1; - } - // TODO(gmackall): Replace uses of deprecated ProrationMode enum values with new // ReplacementMode enum values. // https://github.com/flutter/flutter/issues/128957. @@ -174,14 +152,6 @@ void onDetachedFromActivity() { @Override public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { switch (call.method) { - case MethodNames.START_CONNECTION: - final int handle = (int) call.argument(MethodArgs.HANDLE); - int billingChoiceMode = BillingChoiceMode.PLAY_BILLING_ONLY; - if (call.hasArgument(MethodArgs.BILLING_CHOICE_MODE)) { - billingChoiceMode = call.argument(MethodArgs.BILLING_CHOICE_MODE); - } - startConnection(handle, result, billingChoiceMode); - break; case MethodNames.END_CONNECTION: endConnection(result); break; @@ -503,12 +473,14 @@ private void getConnectionState(final MethodChannel.Result result) { result.success(serialized); } - private void startConnection( - final int handle, final MethodChannel.Result result, int billingChoiceMode) { + @Override + public void startConnection( + @NonNull Long handle, + @NonNull Messages.PlatformBillingChoiceMode billingMode, + @NonNull Messages.Result> result) { if (billingClient == null) { billingClient = - billingClientFactory.createBillingClient( - applicationContext, methodChannel, billingChoiceMode); + billingClientFactory.createBillingClient(applicationContext, methodChannel, billingMode); } billingClient.startConnection( diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index fd2b52899f8..7fae13d2feb 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -18,7 +18,6 @@ import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PURCHASES_ASYNC; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.SHOW_ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG; -import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.START_CONNECTION; import static io.flutter.plugins.inapppurchase.PluginPurchaseListener.ON_PURCHASES_UPDATED; import static io.flutter.plugins.inapppurchase.Translator.fromAlternativeBillingOnlyReportingDetails; import static io.flutter.plugins.inapppurchase.Translator.fromBillingConfig; @@ -49,7 +48,6 @@ import android.app.Activity; import android.content.Context; -import androidx.annotation.Nullable; import com.android.billingclient.api.AcknowledgePurchaseParams; import com.android.billingclient.api.AcknowledgePurchaseResponseListener; import com.android.billingclient.api.AlternativeBillingOnlyAvailabilityListener; @@ -79,8 +77,7 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.BillingChoiceMode; -import io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodArgs; +import io.flutter.plugins.inapppurchase.Messages.PlatformBillingChoiceMode; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; @@ -104,6 +101,7 @@ public class MethodCallHandlerTest { @Mock BillingClient mockBillingClient; @Mock MethodChannel mockMethodChannel; @Spy Result result; + @Spy Messages.Result> connectionResult; @Mock Activity activity; @Mock Context context; @Mock ActivityPluginBinding mockActivityPluginBinding; @@ -114,10 +112,10 @@ public void setUp() { MockitoAnnotations.openMocks(this); // Use the same client no matter if alternative billing is enabled or not. when(factory.createBillingClient( - context, mockMethodChannel, BillingChoiceMode.PLAY_BILLING_ONLY)) + context, mockMethodChannel, PlatformBillingChoiceMode.PLAY_BILLING_ONLY)) .thenReturn(mockBillingClient); when(factory.createBillingClient( - context, mockMethodChannel, BillingChoiceMode.ALTERNATIVE_BILLING_ONLY)) + context, mockMethodChannel, PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY)) .thenReturn(mockBillingClient); methodChannelHandler = new MethodCallHandlerImpl(activity, context, mockMethodChannel, factory); when(mockActivityPluginBinding.getActivity()).thenReturn(activity); @@ -152,19 +150,18 @@ public void isReady_clientDisconnected() { methodChannelHandler.onMethodCall(disconnectCall, mock(Result.class)); Messages.FlutterError exception = - assertThrows( - Messages.FlutterError.class, - () -> methodChannelHandler.isReady()); + assertThrows(Messages.FlutterError.class, () -> methodChannelHandler.isReady()); assertEquals("UNAVAILABLE", exception.code); } @Test public void startConnection() { ArgumentCaptor captor = - mockStartConnection(BillingChoiceMode.PLAY_BILLING_ONLY); - verify(result, never()).success(any()); + mockStartConnection(PlatformBillingChoiceMode.PLAY_BILLING_ONLY); + verify(connectionResult, never()).success(any()); verify(factory, times(1)) - .createBillingClient(context, mockMethodChannel, BillingChoiceMode.PLAY_BILLING_ONLY); + .createBillingClient( + context, mockMethodChannel, PlatformBillingChoiceMode.PLAY_BILLING_ONLY); BillingResult billingResult = BillingResult.newBuilder() @@ -173,43 +170,17 @@ public void startConnection() { .build(); captor.getValue().onBillingSetupFinished(billingResult); - verify(result, times(1)).success(fromBillingResult(billingResult)); + verify(connectionResult, times(1)).success(fromBillingResult(billingResult)); } @Test public void startConnectionAlternativeBillingOnly() { ArgumentCaptor captor = - mockStartConnection(BillingChoiceMode.ALTERNATIVE_BILLING_ONLY); - verify(result, never()).success(any()); + mockStartConnection(PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY); + verify(connectionResult, never()).success(any()); verify(factory, times(1)) .createBillingClient( - context, mockMethodChannel, BillingChoiceMode.ALTERNATIVE_BILLING_ONLY); - - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); - captor.getValue().onBillingSetupFinished(billingResult); - - verify(result, times(1)).success(fromBillingResult(billingResult)); - } - - @Test - public void startConnectionAlternativeBillingUnset() { - // Logic is identical to mockStartConnection but does not set a value for - // ENABLE_ALTERNATIVE_BILLING to verify fallback behavior. - Map arguments = new HashMap<>(); - arguments.put(MethodArgs.HANDLE, 1); - MethodCall call = new MethodCall(START_CONNECTION, arguments); - ArgumentCaptor captor = - ArgumentCaptor.forClass(BillingClientStateListener.class); - doNothing().when(mockBillingClient).startConnection(captor.capture()); - - methodChannelHandler.onMethodCall(call, result); - verify(result, never()).success(any()); - verify(factory, times(1)) - .createBillingClient(context, mockMethodChannel, BillingChoiceMode.PLAY_BILLING_ONLY); + context, mockMethodChannel, PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY); BillingResult billingResult = BillingResult.newBuilder() @@ -218,20 +189,18 @@ public void startConnectionAlternativeBillingUnset() { .build(); captor.getValue().onBillingSetupFinished(billingResult); - verify(result, times(1)).success(fromBillingResult(billingResult)); + verify(connectionResult, times(1)).success(fromBillingResult(billingResult)); } @Test public void startConnection_multipleCalls() { - Map arguments = new HashMap<>(); - arguments.put("handle", 1); - MethodCall call = new MethodCall(START_CONNECTION, arguments); ArgumentCaptor captor = ArgumentCaptor.forClass(BillingClientStateListener.class); doNothing().when(mockBillingClient).startConnection(captor.capture()); - methodChannelHandler.onMethodCall(call, result); - verify(result, never()).success(any()); + methodChannelHandler.startConnection( + 1L, PlatformBillingChoiceMode.PLAY_BILLING_ONLY, connectionResult); + verify(connectionResult, never()).success(any()); BillingResult billingResult1 = BillingResult.newBuilder() .setResponseCode(100) @@ -252,8 +221,8 @@ public void startConnection_multipleCalls() { captor.getValue().onBillingSetupFinished(billingResult2); captor.getValue().onBillingSetupFinished(billingResult3); - verify(result, times(1)).success(fromBillingResult(billingResult1)); - verify(result, times(1)).success(any()); + verify(connectionResult, times(1)).success(fromBillingResult(billingResult1)); + verify(connectionResult, times(1)).success(any()); } @Test @@ -413,14 +382,14 @@ public void showAlternativeBillingOnlyInformationDialog_NullActivity() { @Test public void endConnection() { // Set up a connected BillingClient instance - final int disconnectCallbackHandle = 22; - Map arguments = new HashMap<>(); - arguments.put("handle", disconnectCallbackHandle); - MethodCall connectCall = new MethodCall(START_CONNECTION, arguments); + final long disconnectCallbackHandle = 22; ArgumentCaptor captor = ArgumentCaptor.forClass(BillingClientStateListener.class); doNothing().when(mockBillingClient).startConnection(captor.capture()); - methodChannelHandler.onMethodCall(connectCall, mock(Result.class)); + @SuppressWarnings("unchecked") + final Messages.Result> mockResult = mock(Messages.Result.class); + methodChannelHandler.startConnection( + disconnectCallbackHandle, PlatformBillingChoiceMode.PLAY_BILLING_ONLY, mockResult); final BillingClientStateListener stateListener = captor.getValue(); // Disconnect the connected client @@ -432,7 +401,7 @@ public void endConnection() { verify(result, times(1)).success(any()); verify(mockBillingClient, times(1)).endConnection(); stateListener.onBillingServiceDisconnected(); - Map expectedInvocation = new HashMap<>(); + Map expectedInvocation = new HashMap<>(); expectedInvocation.put("handle", disconnectCallbackHandle); verify(mockMethodChannel, times(1)).invokeMethod(ON_DISCONNECT, expectedInvocation); } @@ -440,7 +409,7 @@ public void endConnection() { @Test public void queryProductDetailsAsync() { // Connect a billing client and set up the product query listeners - establishConnectedBillingClient(/* arguments= */ null, /* result= */ null); + establishConnectedBillingClient(); String productType = BillingClient.ProductType.INAPP; List productsList = asList("id1", "id2"); HashMap arguments = new HashMap<>(); @@ -783,7 +752,7 @@ public void launchBillingFlow_clientDisconnected() { @Test public void launchBillingFlow_productNotFound() { // Try to launch the billing flow for a random product ID - establishConnectedBillingClient(null, null); + establishConnectedBillingClient(); String productId = "foo"; String accountId = "account"; HashMap arguments = new HashMap<>(); @@ -801,7 +770,7 @@ public void launchBillingFlow_productNotFound() { @Test public void launchBillingFlow_oldProductNotFound() { // Try to launch the billing flow for a random product ID - establishConnectedBillingClient(null, null); + establishConnectedBillingClient(); String productId = "foo"; String accountId = "account"; String oldProductId = "oldProduct"; @@ -836,7 +805,7 @@ public void queryPurchases_clientDisconnected() { @Test public void queryPurchases_returns_success() throws Exception { - establishConnectedBillingClient(null, null); + establishConnectedBillingClient(); CountDownLatch lock = new CountDownLatch(1); doAnswer( @@ -888,7 +857,7 @@ public void queryPurchases_returns_success() throws Exception { @Test public void queryPurchaseHistoryAsync() { // Set up an established billing client and all our mocked responses - establishConnectedBillingClient(null, null); + establishConnectedBillingClient(); BillingResult billingResult = BillingResult.newBuilder() .setResponseCode(100) @@ -951,7 +920,7 @@ public void onPurchasesUpdatedListener() { @Test public void consumeAsync() { - establishConnectedBillingClient(null, null); + establishConnectedBillingClient(); BillingResult billingResult = BillingResult.newBuilder() .setResponseCode(100) @@ -980,7 +949,7 @@ public void consumeAsync() { @Test public void acknowledgePurchase() { - establishConnectedBillingClient(null, null); + establishConnectedBillingClient(); BillingResult billingResult = BillingResult.newBuilder() .setResponseCode(100) @@ -1056,49 +1025,39 @@ public void isFutureSupported_false() { } /** - * Call {@link MethodCallHandlerImpl.START_CONNECTION] with startup params. + * Call {@link MethodCallHandlerImpl#startConnection(Long, PlatformBillingChoiceMode, + * Messages.Result>)} with startup params. * - * Defaults to play billing only which is the default. + *

Defaults to play billing only which is the default. */ private ArgumentCaptor mockStartConnection() { - return mockStartConnection(BillingChoiceMode.PLAY_BILLING_ONLY); + return mockStartConnection(PlatformBillingChoiceMode.PLAY_BILLING_ONLY); } /** - * Call {@link MethodCallHandlerImpl.START_CONNECTION] with startup params. - * - *{@link billingChoiceMode} is one of the int value used from {@link BillingChoiceMode}. + * Call {@link MethodCallHandlerImpl#startConnection(Long, PlatformBillingChoiceMode, + * Messages.Result>)} with startup params. */ - private ArgumentCaptor mockStartConnection(int billingChoiceMode) { - Map arguments = new HashMap<>(); - arguments.put(MethodArgs.HANDLE, 1); - arguments.put(MethodArgs.BILLING_CHOICE_MODE, billingChoiceMode); - MethodCall call = new MethodCall(START_CONNECTION, arguments); + private ArgumentCaptor mockStartConnection( + PlatformBillingChoiceMode billingChoiceMode) { ArgumentCaptor captor = ArgumentCaptor.forClass(BillingClientStateListener.class); doNothing().when(mockBillingClient).startConnection(captor.capture()); - methodChannelHandler.onMethodCall(call, result); + methodChannelHandler.startConnection(1L, billingChoiceMode, connectionResult); return captor; } - private void establishConnectedBillingClient( - @Nullable Map arguments, @Nullable Result result) { - if (arguments == null) { - arguments = new HashMap<>(); - arguments.put(MethodArgs.HANDLE, 1); - } - if (result == null) { - result = mock(Result.class); - } - - MethodCall connectCall = new MethodCall(START_CONNECTION, arguments); - methodChannelHandler.onMethodCall(connectCall, result); + private void establishConnectedBillingClient() { + @SuppressWarnings("unchecked") + final Messages.Result> mockResult = mock(Messages.Result.class); + methodChannelHandler.startConnection( + 1L, PlatformBillingChoiceMode.PLAY_BILLING_ONLY, mockResult); } private void queryForProducts(List productIdList) { // Set up the query method call - establishConnectedBillingClient(/* arguments= */ null, /* result= */ null); + establishConnectedBillingClient(); HashMap arguments = new HashMap<>(); String productType = BillingClient.ProductType.INAPP; List> productList = buildProductMap(productIdList, productType); 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 85efbc62fd5..2cb0b7db54c 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 @@ -70,7 +70,7 @@ class BillingClientManager { StreamController.broadcast(); BillingChoiceMode _billingChoiceMode; - BillingClientFactory _billingClientFactory; + final BillingClientFactory _billingClientFactory; bool _isConnecting = false; bool _isDisposed = false; 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 c50374efb9e..af720a82da9 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 @@ -11,6 +11,7 @@ import 'package:json_annotation/json_annotation.dart'; import '../../billing_client_wrappers.dart'; import '../channel.dart'; import '../messages.g.dart'; +import '../pigeon_converters.dart'; import 'billing_config_wrapper.dart'; part 'billing_client_wrapper.g.dart'; @@ -119,15 +120,10 @@ class BillingClient { final List disconnectCallbacks = _callbacks[_kOnBillingServiceDisconnected] ??= []; disconnectCallbacks.add(onBillingServiceDisconnected); - return BillingResultWrapper.fromJson((await channel - .invokeMapMethod( - 'BillingClient#startConnection(BillingClientStateListener)', - { - 'handle': disconnectCallbacks.length - 1, - 'billingChoiceMode': - const BillingChoiceModeConverter().toJson(billingChoiceMode), - })) ?? - {}); + return BillingResultWrapper.fromJson((await _hostApi.startConnection( + disconnectCallbacks.length - 1, + platformBillingChoiceMode(billingChoiceMode))) + .cast()); } /// Calls diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart index 604148321a0..a86aec00952 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -18,6 +18,17 @@ PlatformException _createConnectionError(String channelName) { ); } +/// Pigeon version of BillingChoiceMode. +enum PlatformBillingChoiceMode { + /// Billing through google play. + /// + /// Default state. + playBillingOnly, + + /// Billing through app provided flow. + alternativeBillingOnly, +} + class InAppPurchaseApi { /// Constructor for [InAppPurchaseApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default @@ -58,4 +69,35 @@ class InAppPurchaseApi { return (__pigeon_replyList[0] as bool?)!; } } + + Future> startConnection( + int callbackHandle, PlatformBillingChoiceMode billingMode) async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.startConnection'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = await __pigeon_channel + .send([callbackHandle, billingMode.index]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as Map?)! + .cast(); + } + } } diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart new file mode 100644 index 00000000000..f7eee7c057f --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart @@ -0,0 +1,16 @@ +// 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 'billing_client_wrappers/billing_client_wrapper.dart'; +import 'messages.g.dart'; + +/// Converts a [BillingChoiceMode] to the Pigeon equivalent. +PlatformBillingChoiceMode platformBillingChoiceMode(BillingChoiceMode mode) { + return switch (mode) { + BillingChoiceMode.playBillingOnly => + PlatformBillingChoiceMode.playBillingOnly, + BillingChoiceMode.alternativeBillingOnly => + PlatformBillingChoiceMode.alternativeBillingOnly, + }; +} diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart index 203194905fa..7f6c9f994f1 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -11,8 +11,25 @@ import 'package:pigeon/pigeon.dart'; 'android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java', copyrightHeader: 'pigeons/copyright.txt', )) + +/// Pigeon version of BillingChoiceMode. +enum PlatformBillingChoiceMode { + /// Billing through google play. + /// + /// Default state. + playBillingOnly, + + /// Billing through app provided flow. + alternativeBillingOnly, +} + @HostApi() abstract class InAppPurchaseApi { /// Wraps BillingClient#isReady. bool isReady(); + + // XXX half converted; should return an object. + @async + Map startConnection( + int callbackHandle, PlatformBillingChoiceMode billingMode); } 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 b53bb5c96c3..52d663aca2c 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 @@ -9,19 +9,19 @@ 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 'package:in_app_purchase_android/src/messages.g.dart'; +import 'package:mockito/mockito.dart'; import '../stub_in_app_purchase_platform.dart'; -import 'purchase_wrapper_test.dart'; +import 'billing_client_wrapper_test.mocks.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); + late MockInAppPurchaseApi mockApi; late BillingClientManager manager; - late Completer connectedCompleter; - const String startConnectionCall = - 'BillingClient#startConnection(BillingClientStateListener)'; const String endConnectionCall = 'BillingClient#endConnection()'; const String onBillingServiceDisconnectedCallback = 'BillingClientStateListener#onBillingServiceDisconnected()'; @@ -32,26 +32,27 @@ void main() { 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(); + mockApi = MockInAppPurchaseApi(); + manager = BillingClientManager( + billingClientFactory: (PurchasesUpdatedListener listener) => + BillingClient(listener, api: mockApi)); }); tearDown(() => stubPlatform.reset()); group('BillingClientWrapper', () { test('connects on initialization', () { - expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(1)); + verify(mockApi.startConnection(any, any)).called(1); }); test('waits for connection before executing the operations', () async { + final Completer connectedCompleter = Completer(); + when(mockApi.startConnection(any, any)).thenAnswer((_) async { + connectedCompleter.complete(); + return {}; + }); + final Completer calledCompleter1 = Completer(); final Completer calledCompleter2 = Completer(); unawaited(manager.runWithClient((BillingClient _) async { @@ -70,7 +71,6 @@ void main() { test('re-connects when client sends onBillingServiceDisconnected', () async { - connectedCompleter.complete(); // Ensures all asynchronous connected code finishes. await manager.runWithClientNonRetryable((_) async {}); @@ -78,12 +78,11 @@ void main() { const MethodCall(onBillingServiceDisconnectedCallback, {'handle': 0}), ); - expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(2)); + verify(mockApi.startConnection(any, any)).called(2); }); test('re-connects when host calls reconnectWithBillingChoiceMode', () async { - connectedCompleter.complete(); // Ensures all asynchronous connected code finishes. await manager.runWithClientNonRetryable((_) async {}); @@ -93,13 +92,7 @@ void main() { expect(stubPlatform.countPreviousCalls(endConnectionCall), equals(1)); stubPlatform.reset(); - - late Map arguments; - stubPlatform.addResponse( - name: startConnectionCall, - additionalStepBeforeReturn: (dynamic value) => - arguments = value as Map, - ); + clearInteractions(mockApi); /// Fake the disconnect that we would expect from a endConnectionCall. await manager.client.callHandler( @@ -107,14 +100,17 @@ void main() { {'handle': 0}), ); // Verify that after connection ended reconnect was called. - expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(1)); - expect(arguments['billingChoiceMode'], 1); + final VerificationResult result = + verify(mockApi.startConnection(any, captureAny)); + expect(result.captured.single, + PlatformBillingChoiceMode.alternativeBillingOnly); }); test( 're-connects when operation returns BillingResponse.serviceDisconnected', () async { - connectedCompleter.complete(); + clearInteractions(mockApi); + int timesCalled = 0; final BillingResultWrapper result = await manager.runWithClient( (BillingClient _) async { @@ -126,16 +122,16 @@ void main() { ); }, ); - expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(2)); + verify(mockApi.startConnection(any, any)).called(1); expect(timesCalled, equals(2)); expect(result.responseCode, equals(BillingResponse.ok)); }, ); test('does not re-connect when disposed', () { - connectedCompleter.complete(); + clearInteractions(mockApi); manager.dispose(); - expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(1)); + verifyNever(mockApi.startConnection(any, any)); expect(stubPlatform.countPreviousCalls(endConnectionCall), equals(1)); }); }); diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index 22a415000b4..140c50d2d96 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -2,6 +2,8 @@ // 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_test/flutter_test.dart'; import 'package:in_app_purchase_android/billing_client_wrappers.dart'; @@ -30,7 +32,7 @@ const PurchaseWrapper dummyOldPurchase = PurchaseWrapper( purchaseState: PurchaseStateWrapper.purchased, ); -@GenerateMocks([InAppPurchaseApi]) +@GenerateNiceMocks(>[MockSpec()]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -80,14 +82,11 @@ void main() { }); group('startConnection', () { - const String methodName = - 'BillingClient#startConnection(BillingClientStateListener)'; test('returns BillingResultWrapper', () async { const String debugMessage = 'dummy message'; const BillingResponse responseCode = BillingResponse.developerError; - stubPlatform.addResponse( - name: methodName, - value: { + when(mockApi.startConnection(any, any)).thenAnswer( + (_) async => { 'responseCode': const BillingResponseConverter().toJson(responseCode), 'debugMessage': debugMessage, }, @@ -101,59 +100,22 @@ void main() { equals(billingResult)); }); - test('passes handle to onBillingServiceDisconnected', () async { - const String debugMessage = 'dummy message'; - const BillingResponse responseCode = BillingResponse.developerError; - stubPlatform.addResponse( - name: methodName, - value: { - 'responseCode': const BillingResponseConverter().toJson(responseCode), - 'debugMessage': debugMessage, - }, - ); + test('passes default values to onBillingServiceDisconnected', () async { await billingClient.startConnection(onBillingServiceDisconnected: () {}); - final MethodCall call = stubPlatform.previousCallMatching(methodName); - expect( - call.arguments, - equals({ - 'handle': 0, - 'billingChoiceMode': 0, - })); + + final VerificationResult result = + verify(mockApi.startConnection(captureAny, captureAny)); + expect(result.captured[0], 0); + expect(result.captured[1], PlatformBillingChoiceMode.playBillingOnly); }); test('passes billingChoiceMode when set', () async { - const String debugMessage = 'dummy message'; - const BillingResponse responseCode = BillingResponse.developerError; - stubPlatform.addResponse( - name: methodName, - value: { - 'responseCode': const BillingResponseConverter().toJson(responseCode), - 'debugMessage': debugMessage, - }, - ); await billingClient.startConnection( onBillingServiceDisconnected: () {}, billingChoiceMode: BillingChoiceMode.alternativeBillingOnly); - final MethodCall call = stubPlatform.previousCallMatching(methodName); - expect( - call.arguments, - equals({ - 'handle': 0, - 'billingChoiceMode': 1, - })); - }); - test('handles method channel returning null', () async { - stubPlatform.addResponse( - name: methodName, - ); - - expect( - await billingClient.startConnection( - onBillingServiceDisconnected: () {}), - equals(const BillingResultWrapper( - responseCode: BillingResponse.error, - debugMessage: kInvalidBillingResultErrorMessage))); + expect(verify(mockApi.startConnection(any, captureAny)).captured.first, + PlatformBillingChoiceMode.alternativeBillingOnly); }); }); diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart index 48ff3c73c9c..ced1ccb3440 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart @@ -25,10 +25,6 @@ import 'package:mockito/mockito.dart' as _i1; /// /// See the documentation for Mockito's code generation for more information. class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { - MockInAppPurchaseApi() { - _i1.throwOnMissingStub(this); - } - @override _i3.Future isReady() => (super.noSuchMethod( Invocation.method( @@ -36,5 +32,25 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { [], ), returnValue: _i3.Future.value(false), + returnValueForMissingStub: _i3.Future.value(false), ) as _i3.Future); + + @override + _i3.Future> startConnection( + int? callbackHandle, + _i2.PlatformBillingChoiceMode? billingMode, + ) => + (super.noSuchMethod( + Invocation.method( + #startConnection, + [ + callbackHandle, + billingMode, + ], + ), + returnValue: + _i3.Future>.value({}), + returnValueForMissingStub: + _i3.Future>.value({}), + ) as _i3.Future>); } 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 718bd3cdde1..6d03452b3ca 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 @@ -9,8 +9,11 @@ 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/billing_config_wrapper.dart'; import 'package:in_app_purchase_android/src/channel.dart'; +import 'package:in_app_purchase_android/src/messages.g.dart'; +import 'package:mockito/mockito.dart'; import 'billing_client_wrappers/billing_client_wrapper_test.dart'; +import 'billing_client_wrappers/billing_client_wrapper_test.mocks.dart'; import 'billing_client_wrappers/purchase_wrapper_test.dart'; import 'stub_in_app_purchase_platform.dart'; @@ -18,9 +21,8 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); + late MockInAppPurchaseApi mockApi; late InAppPurchaseAndroidPlatformAddition iapAndroidPlatformAddition; - const String startConnectionCall = - 'BillingClient#startConnection(BillingClientStateListener)'; const String endConnectionCall = 'BillingClient#endConnection()'; const String onBillingServiceDisconnectedCallback = 'BillingClientStateListener#onBillingServiceDisconnected()'; @@ -33,16 +35,11 @@ void main() { setUp(() { widgets.WidgetsFlutterBinding.ensureInitialized(); - - const String debugMessage = 'dummy message'; - const BillingResponse responseCode = BillingResponse.ok; - const BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: startConnectionCall, - value: buildBillingResultMap(expectedBillingResult)); stubPlatform.addResponse(name: endConnectionCall); - manager = BillingClientManager(); + mockApi = MockInAppPurchaseApi(); + manager = BillingClientManager( + billingClientFactory: (PurchasesUpdatedListener listener) => + BillingClient(listener, api: mockApi)); iapAndroidPlatformAddition = InAppPurchaseAndroidPlatformAddition(manager); }); @@ -86,14 +83,9 @@ void main() { }); group('setBillingChoice', () { - late Map arguments; test('setAlternativeBillingOnlyState', () async { stubPlatform.reset(); - stubPlatform.addResponse( - name: startConnectionCall, - additionalStepBeforeReturn: (dynamic value) => - arguments = value as Map, - ); + clearInteractions(mockApi); stubPlatform.addResponse(name: endConnectionCall); await iapAndroidPlatformAddition .setBillingChoice(BillingChoiceMode.alternativeBillingOnly); @@ -104,20 +96,16 @@ void main() { {'handle': 0}), ); // Verify that after connection ended reconnect was called. - expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(2)); - expect( - arguments['billingChoiceMode'], - const BillingChoiceModeConverter() - .toJson(BillingChoiceMode.alternativeBillingOnly)); + final VerificationResult result = + verify(mockApi.startConnection(any, captureAny)); + expect(result.callCount, equals(2)); + expect(result.captured.last, + PlatformBillingChoiceMode.alternativeBillingOnly); }); test('setPlayBillingState', () async { stubPlatform.reset(); - stubPlatform.addResponse( - name: startConnectionCall, - additionalStepBeforeReturn: (dynamic value) => - arguments = value as Map, - ); + clearInteractions(mockApi); stubPlatform.addResponse(name: endConnectionCall); await iapAndroidPlatformAddition .setBillingChoice(BillingChoiceMode.playBillingOnly); @@ -128,11 +116,10 @@ void main() { {'handle': 0}), ); // Verify that after connection ended reconnect was called. - expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(2)); - expect( - arguments['billingChoiceMode'], - const BillingChoiceModeConverter() - .toJson(BillingChoiceMode.playBillingOnly)); + final VerificationResult result = + verify(mockApi.startConnection(any, captureAny)); + expect(result.callCount, equals(2)); + expect(result.captured.last, PlatformBillingChoiceMode.playBillingOnly); }); }); 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 b91585814fd..282f6f74d86 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,8 +24,6 @@ void main() { final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); late MockInAppPurchaseApi mockApi; late InAppPurchaseAndroidPlatform iapAndroidPlatform; - const String startConnectionCall = - 'BillingClient#startConnection(BillingClientStateListener)'; const String endConnectionCall = 'BillingClient#endConnection()'; const String acknowledgePurchaseCall = 'BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; @@ -39,14 +37,6 @@ void main() { setUp(() { widgets.WidgetsFlutterBinding.ensureInitialized(); - - const String debugMessage = 'dummy message'; - const BillingResponse responseCode = BillingResponse.ok; - const BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: startConnectionCall, - value: buildBillingResultMap(expectedBillingResult)); stubPlatform.addResponse(name: endConnectionCall); mockApi = MockInAppPurchaseApi(); @@ -72,7 +62,7 @@ void main() { group('connection management', () { test('connects on initialization', () { //await iapAndroidPlatform.isAvailable(); - expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(1)); + verify(mockApi.startConnection(any, any)).called(1); }); test('re-connects when client sends onBillingServiceDisconnected', () { @@ -80,7 +70,7 @@ void main() { const MethodCall(onBillingServiceDisconnectedCallback, {'handle': 0}), ); - expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(2)); + verify(mockApi.startConnection(any, any)).called(2); }); test( @@ -96,12 +86,10 @@ void main() { ), ), ); - stubPlatform.addResponse( - name: startConnectionCall, - value: okValue, - additionalStepBeforeReturn: (dynamic _) => stubPlatform.addResponse( - name: acknowledgePurchaseCall, value: okValue), - ); + when(mockApi.startConnection(any, any)).thenAnswer((_) async { + stubPlatform.addResponse(name: acknowledgePurchaseCall, value: okValue); + return okValue; + }); final PurchaseDetails purchase = GooglePlayPurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase) .first; @@ -111,7 +99,7 @@ void main() { stubPlatform.countPreviousCalls(acknowledgePurchaseCall), equals(2), ); - expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(2)); + verify(mockApi.startConnection(any, any)).called(2); expect(result.responseCode, equals(BillingResponse.ok)); }); }); From 85f20713a18804a69763d5668c8d60bafa2b47aa Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 4 Mar 2024 15:06:13 -0500 Subject: [PATCH 03/25] Finish converting startConnection --- .../plugins/inapppurchase/Messages.java | 133 +++++++++++++++++- .../inapppurchase/MethodCallHandlerImpl.java | 5 +- .../plugins/inapppurchase/Translator.java | 8 ++ .../inapppurchase/MethodCallHandlerTest.java | 28 +++- .../billing_client_wrapper.dart | 7 +- .../src/in_app_purchase_android_platform.dart | 1 - .../lib/src/messages.g.dart | 58 +++++++- .../lib/src/pigeon_converters.dart | 10 +- .../pigeons/messages.dart | 14 +- .../billing_client_wrapper_test.dart | 3 - 10 files changed, 236 insertions(+), 31 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java index 0548fc7201f..68a4683c8c7 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -6,6 +6,9 @@ package io.flutter.plugins.inapppurchase; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.CLASS; + import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -13,8 +16,11 @@ import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MessageCodec; import io.flutter.plugin.common.StandardMessageCodec; +import java.io.ByteArrayOutputStream; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Map; /** Generated class from Pigeon. */ @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) @@ -53,6 +59,10 @@ protected static ArrayList wrapError(@NonNull Throwable exception) { return errorList; } + @Target(METHOD) + @Retention(CLASS) + @interface CanIgnoreReturnValue {} + /** Pigeon version of BillingChoiceMode. */ public enum PlatformBillingChoiceMode { /** @@ -71,6 +81,88 @@ private PlatformBillingChoiceMode(final int index) { } } + /** + * Pigeon version of BillingResult. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PlatformBillingResult { + private @NonNull Long responseCode; + + public @NonNull Long getResponseCode() { + return responseCode; + } + + public void setResponseCode(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"responseCode\" is null."); + } + this.responseCode = setterArg; + } + + private @NonNull String debugMessage; + + public @NonNull String getDebugMessage() { + return debugMessage; + } + + public void setDebugMessage(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"debugMessage\" is null."); + } + this.debugMessage = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PlatformBillingResult() {} + + public static final class Builder { + + private @Nullable Long responseCode; + + @CanIgnoreReturnValue + public @NonNull Builder setResponseCode(@NonNull Long setterArg) { + this.responseCode = setterArg; + return this; + } + + private @Nullable String debugMessage; + + @CanIgnoreReturnValue + public @NonNull Builder setDebugMessage(@NonNull String setterArg) { + this.debugMessage = setterArg; + return this; + } + + public @NonNull PlatformBillingResult build() { + PlatformBillingResult pigeonReturn = new PlatformBillingResult(); + pigeonReturn.setResponseCode(responseCode); + pigeonReturn.setDebugMessage(debugMessage); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(responseCode); + toListResult.add(debugMessage); + return toListResult; + } + + static @NonNull PlatformBillingResult fromList(@NonNull ArrayList list) { + PlatformBillingResult pigeonResult = new PlatformBillingResult(); + Object responseCode = list.get(0); + pigeonResult.setResponseCode( + (responseCode == null) + ? null + : ((responseCode instanceof Integer) ? (Integer) responseCode : (Long) responseCode)); + Object debugMessage = list.get(1); + pigeonResult.setDebugMessage((String) debugMessage); + return pigeonResult; + } + } + /** Asynchronous error handling return type for non-nullable API method returns. */ public interface Result { /** Success case callback method for handling returns. */ @@ -95,20 +187,47 @@ public interface VoidResult { /** Failure case callback method for handling errors. */ void error(@NonNull Throwable error); } + + private static class InAppPurchaseApiCodec extends StandardMessageCodec { + public static final InAppPurchaseApiCodec INSTANCE = new InAppPurchaseApiCodec(); + + private InAppPurchaseApiCodec() {} + + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return PlatformBillingResult.fromList((ArrayList) readValue(buffer)); + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof PlatformBillingResult) { + stream.write(128); + writeValue(stream, ((PlatformBillingResult) value).toList()); + } else { + super.writeValue(stream, value); + } + } + } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface InAppPurchaseApi { /** Wraps BillingClient#isReady. */ @NonNull Boolean isReady(); - + /** Wraps BillingClient#startConnection(BillingClientStateListener). */ void startConnection( @NonNull Long callbackHandle, @NonNull PlatformBillingChoiceMode billingMode, - @NonNull Result> result); + @NonNull Result result); /** The codec used by InAppPurchaseApi. */ static @NonNull MessageCodec getCodec() { - return new StandardMessageCodec(); + return InAppPurchaseApiCodec.INSTANCE; } /** * Sets up an instance of `InAppPurchaseApi` to handle messages through the `binaryMessenger`. @@ -151,9 +270,9 @@ static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable InAppPurch Number callbackHandleArg = (Number) args.get(0); PlatformBillingChoiceMode billingModeArg = PlatformBillingChoiceMode.values()[(int) args.get(1)]; - Result> resultCallback = - new Result>() { - public void success(Map result) { + Result resultCallback = + new Result() { + public void success(PlatformBillingResult result) { wrapped.add(0, result); reply.reply(wrapped); } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index 3a788a76830..505775e5a42 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -12,6 +12,7 @@ import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; +import static io.flutter.plugins.inapppurchase.Translator.pigeonBillingResultFromBillingResult; import static io.flutter.plugins.inapppurchase.Translator.toProductList; import android.app.Activity; @@ -477,7 +478,7 @@ private void getConnectionState(final MethodChannel.Result result) { public void startConnection( @NonNull Long handle, @NonNull Messages.PlatformBillingChoiceMode billingMode, - @NonNull Messages.Result> result) { + @NonNull Messages.Result result) { if (billingClient == null) { billingClient = billingClientFactory.createBillingClient(applicationContext, methodChannel, billingMode); @@ -496,7 +497,7 @@ public void onBillingSetupFinished(@NonNull BillingResult billingResult) { alreadyFinished = true; // Consider the fact that we've finished a success, leave it to the Dart side to // validate the responseCode. - result.success(fromBillingResult(billingResult)); + result.success(pigeonBillingResultFromBillingResult(billingResult)); } @Override diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java index 8c80b1797ed..7a66051b4e0 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java @@ -233,6 +233,14 @@ static HashMap fromBillingResult(BillingResult billingResult) { return info; } + static Messages.PlatformBillingResult pigeonBillingResultFromBillingResult( + BillingResult billingResult) { + Messages.PlatformBillingResult.Builder builder = new Messages.PlatformBillingResult.Builder(); + builder.setResponseCode((long) billingResult.getResponseCode()); + builder.setDebugMessage(billingResult.getDebugMessage()); + return builder.build(); + } + /** Converter from {@link BillingResult} and {@link BillingConfig} to map. */ static HashMap fromBillingConfig( BillingResult result, BillingConfig billingConfig) { diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index 7fae13d2feb..8b442fe4ec5 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -78,6 +78,7 @@ import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingChoiceMode; +import io.flutter.plugins.inapppurchase.Messages.PlatformBillingResult; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; @@ -101,7 +102,7 @@ public class MethodCallHandlerTest { @Mock BillingClient mockBillingClient; @Mock MethodChannel mockMethodChannel; @Spy Result result; - @Spy Messages.Result> connectionResult; + @Spy Messages.Result connectionResult; @Mock Activity activity; @Mock Context context; @Mock ActivityPluginBinding mockActivityPluginBinding; @@ -170,7 +171,12 @@ public void startConnection() { .build(); captor.getValue().onBillingSetupFinished(billingResult); - verify(connectionResult, times(1)).success(fromBillingResult(billingResult)); + ArgumentCaptor resultCaptor = + ArgumentCaptor.forClass(PlatformBillingResult.class); + verify(connectionResult, times(1)).success(resultCaptor.capture()); + assertEquals( + resultCaptor.getValue().getResponseCode().longValue(), billingResult.getResponseCode()); + assertEquals(resultCaptor.getValue().getDebugMessage(), billingResult.getDebugMessage()); } @Test @@ -189,7 +195,12 @@ public void startConnectionAlternativeBillingOnly() { .build(); captor.getValue().onBillingSetupFinished(billingResult); - verify(connectionResult, times(1)).success(fromBillingResult(billingResult)); + ArgumentCaptor resultCaptor = + ArgumentCaptor.forClass(PlatformBillingResult.class); + verify(connectionResult, times(1)).success(resultCaptor.capture()); + assertEquals( + resultCaptor.getValue().getResponseCode().longValue(), billingResult.getResponseCode()); + assertEquals(resultCaptor.getValue().getDebugMessage(), billingResult.getDebugMessage()); } @Test @@ -221,7 +232,12 @@ public void startConnection_multipleCalls() { captor.getValue().onBillingSetupFinished(billingResult2); captor.getValue().onBillingSetupFinished(billingResult3); - verify(connectionResult, times(1)).success(fromBillingResult(billingResult1)); + ArgumentCaptor resultCaptor = + ArgumentCaptor.forClass(PlatformBillingResult.class); + verify(connectionResult, times(1)).success(resultCaptor.capture()); + assertEquals( + resultCaptor.getValue().getResponseCode().longValue(), billingResult1.getResponseCode()); + assertEquals(resultCaptor.getValue().getDebugMessage(), billingResult1.getDebugMessage()); verify(connectionResult, times(1)).success(any()); } @@ -387,7 +403,7 @@ public void endConnection() { ArgumentCaptor.forClass(BillingClientStateListener.class); doNothing().when(mockBillingClient).startConnection(captor.capture()); @SuppressWarnings("unchecked") - final Messages.Result> mockResult = mock(Messages.Result.class); + final Messages.Result mockResult = mock(Messages.Result.class); methodChannelHandler.startConnection( disconnectCallbackHandle, PlatformBillingChoiceMode.PLAY_BILLING_ONLY, mockResult); final BillingClientStateListener stateListener = captor.getValue(); @@ -1050,7 +1066,7 @@ private ArgumentCaptor mockStartConnection( private void establishConnectedBillingClient() { @SuppressWarnings("unchecked") - final Messages.Result> mockResult = mock(Messages.Result.class); + final Messages.Result mockResult = mock(Messages.Result.class); methodChannelHandler.startConnection( 1L, PlatformBillingChoiceMode.PLAY_BILLING_ONLY, mockResult); } 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 af720a82da9..822babc9087 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 @@ -120,10 +120,9 @@ class BillingClient { final List disconnectCallbacks = _callbacks[_kOnBillingServiceDisconnected] ??= []; disconnectCallbacks.add(onBillingServiceDisconnected); - return BillingResultWrapper.fromJson((await _hostApi.startConnection( - disconnectCallbacks.length - 1, - platformBillingChoiceMode(billingChoiceMode))) - .cast()); + return resultWrapperFromPlatform(await _hostApi.startConnection( + disconnectCallbacks.length - 1, + platformBillingChoiceMode(billingChoiceMode))); } /// 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 56937c3aa57..9663aa414d5 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 @@ -11,7 +11,6 @@ import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_inte import '../billing_client_wrappers.dart'; import '../in_app_purchase_android.dart'; -import 'billing_client_wrappers/billing_client_manager.dart'; /// [IAPError.code] code for failed purchases. const String kPurchaseErrorCode = 'purchase_error'; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart index a86aec00952..e41259fb5a5 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -29,6 +29,56 @@ enum PlatformBillingChoiceMode { alternativeBillingOnly, } +/// Pigeon version of BillingResult. +class PlatformBillingResult { + PlatformBillingResult({ + required this.responseCode, + required this.debugMessage, + }); + + int responseCode; + + String debugMessage; + + Object encode() { + return [ + responseCode, + debugMessage, + ]; + } + + static PlatformBillingResult decode(Object result) { + result as List; + return PlatformBillingResult( + responseCode: result[0]! as int, + debugMessage: result[1]! as String, + ); + } +} + +class _InAppPurchaseApiCodec extends StandardMessageCodec { + const _InAppPurchaseApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is PlatformBillingResult) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return PlatformBillingResult.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + class InAppPurchaseApi { /// Constructor for [InAppPurchaseApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default @@ -38,7 +88,7 @@ class InAppPurchaseApi { final BinaryMessenger? __pigeon_binaryMessenger; static const MessageCodec pigeonChannelCodec = - StandardMessageCodec(); + _InAppPurchaseApiCodec(); /// Wraps BillingClient#isReady. Future isReady() async { @@ -70,7 +120,8 @@ class InAppPurchaseApi { } } - Future> startConnection( + /// Wraps BillingClient#startConnection(BillingClientStateListener). + Future startConnection( int callbackHandle, PlatformBillingChoiceMode billingMode) async { const String __pigeon_channelName = 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.startConnection'; @@ -96,8 +147,7 @@ class InAppPurchaseApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (__pigeon_replyList[0] as Map?)! - .cast(); + return (__pigeon_replyList[0] as PlatformBillingResult?)!; } } } diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart index f7eee7c057f..6c548ed19e3 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'billing_client_wrappers/billing_client_wrapper.dart'; +import '../billing_client_wrappers.dart'; import 'messages.g.dart'; /// Converts a [BillingChoiceMode] to the Pigeon equivalent. @@ -14,3 +14,11 @@ PlatformBillingChoiceMode platformBillingChoiceMode(BillingChoiceMode mode) { PlatformBillingChoiceMode.alternativeBillingOnly, }; } + +/// Converts a [BillingResultWrapper] to the Pigeon equivalent. +BillingResultWrapper resultWrapperFromPlatform(PlatformBillingResult result) { + return BillingResultWrapper( + responseCode: + const BillingResponseConverter().fromJson(result.responseCode), + debugMessage: result.debugMessage); +} diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart index 7f6c9f994f1..0558e0a87f6 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -12,7 +12,15 @@ import 'package:pigeon/pigeon.dart'; copyrightHeader: 'pigeons/copyright.txt', )) -/// Pigeon version of BillingChoiceMode. +/// Pigeon version of Java BillingResult. +class PlatformBillingResult { + PlatformBillingResult( + {required this.responseCode, required this.debugMessage}); + final int responseCode; + final String debugMessage; +} + +/// Pigeon version of billing_client_wrapper.dart's BillingChoiceMode. enum PlatformBillingChoiceMode { /// Billing through google play. /// @@ -28,8 +36,8 @@ abstract class InAppPurchaseApi { /// Wraps BillingClient#isReady. bool isReady(); - // XXX half converted; should return an object. + /// Wraps BillingClient#startConnection(BillingClientStateListener). @async - Map startConnection( + PlatformBillingResult startConnection( int callbackHandle, PlatformBillingChoiceMode billingMode); } diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index 140c50d2d96..1d26e914eec 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -2,9 +2,6 @@ // 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_test/flutter_test.dart'; import 'package:in_app_purchase_android/billing_client_wrappers.dart'; import 'package:in_app_purchase_android/src/billing_client_wrappers/billing_config_wrapper.dart'; From 4024906d685a764e99f5f5fbc3ede3b4fb3c6b2e Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 4 Mar 2024 15:17:44 -0500 Subject: [PATCH 04/25] Convert endConnection --- .../plugins/inapppurchase/Messages.java | 29 +++++++++++++++++-- .../inapppurchase/MethodCallHandlerImpl.java | 8 ++--- .../inapppurchase/MethodCallHandlerTest.java | 20 ++++--------- .../billing_client_wrapper.dart | 2 +- .../lib/src/messages.g.dart | 29 +++++++++++++++++-- .../pigeons/messages.dart | 3 ++ .../billing_client_manager_test.dart | 16 ++-------- .../billing_client_wrapper_test.dart | 6 ++-- ...rchase_android_platform_addition_test.dart | 4 --- ...in_app_purchase_android_platform_test.dart | 2 -- 10 files changed, 70 insertions(+), 49 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java index 68a4683c8c7..1545ebc2568 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -63,7 +63,7 @@ protected static ArrayList wrapError(@NonNull Throwable exception) { @Retention(CLASS) @interface CanIgnoreReturnValue {} - /** Pigeon version of BillingChoiceMode. */ + /** Pigeon version of billing_client_wrapper.dart's BillingChoiceMode. */ public enum PlatformBillingChoiceMode { /** * Billing through google play. @@ -82,7 +82,7 @@ private PlatformBillingChoiceMode(final int index) { } /** - * Pigeon version of BillingResult. + * Pigeon version of Java BillingResult. * *

Generated class from Pigeon that represents data sent in messages. */ @@ -224,6 +224,8 @@ void startConnection( @NonNull Long callbackHandle, @NonNull PlatformBillingChoiceMode billingMode, @NonNull Result result); + /** Wraps BillingClient#endConnection(BillingClientStateListener). */ + void endConnection(); /** The codec used by InAppPurchaseApi. */ static @NonNull MessageCodec getCodec() { @@ -292,6 +294,29 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.endConnection", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.endConnection(); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index 505775e5a42..158e1e27473 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -51,7 +51,6 @@ class MethodCallHandlerImpl @VisibleForTesting static final class MethodNames { - static final String END_CONNECTION = "BillingClient#endConnection()"; static final String ON_DISCONNECT = "BillingClientStateListener#onBillingServiceDisconnected()"; static final String QUERY_PRODUCT_DETAILS = "BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener)"; @@ -153,9 +152,6 @@ void onDetachedFromActivity() { @Override public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { switch (call.method) { - case MethodNames.END_CONNECTION: - endConnection(result); - break; case MethodNames.QUERY_PRODUCT_DETAILS: List productList = toProductList(call.argument("productList")); queryProductDetailsAsync(productList, result); @@ -256,9 +252,9 @@ private void getBillingConfig(final MethodChannel.Result result) { }); } - private void endConnection(final MethodChannel.Result result) { + @Override + public void endConnection() { endBillingClientConnection(); - result.success(null); } private void endBillingClientConnection() { diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index 8b442fe4ec5..f40ec1e552e 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -8,7 +8,6 @@ import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.ACKNOWLEDGE_PURCHASE; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.CONSUME_PURCHASE_ASYNC; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS; -import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.END_CONNECTION; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.GET_BILLING_CONFIG; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.IS_ALTERNATIVE_BILLING_ONLY_AVAILABLE; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.IS_FEATURE_SUPPORTED; @@ -147,8 +146,7 @@ public void isReady_false() { @Test public void isReady_clientDisconnected() { - MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); - methodChannelHandler.onMethodCall(disconnectCall, mock(Result.class)); + methodChannelHandler.endConnection(); Messages.FlutterError exception = assertThrows(Messages.FlutterError.class, () -> methodChannelHandler.isReady()); @@ -409,12 +407,10 @@ public void endConnection() { final BillingClientStateListener stateListener = captor.getValue(); // Disconnect the connected client - MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); - methodChannelHandler.onMethodCall(disconnectCall, result); + methodChannelHandler.endConnection(); // Verify that the client is disconnected and that the OnDisconnect callback has // been triggered - verify(result, times(1)).success(any()); verify(mockBillingClient, times(1)).endConnection(); stateListener.onBillingServiceDisconnected(); Map expectedInvocation = new HashMap<>(); @@ -462,8 +458,7 @@ public void queryProductDetailsAsync() { @Test public void queryProductDetailsAsync_clientDisconnected() { // Disconnect the Billing client and prepare a queryProductDetails call - MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); - methodChannelHandler.onMethodCall(disconnectCall, mock(Result.class)); + methodChannelHandler.endConnection(); String productType = BillingClient.ProductType.INAPP; List productsList = asList("id1", "id2"); HashMap arguments = new HashMap<>(); @@ -749,8 +744,7 @@ public void launchBillingFlow_ok_Full() { @Test public void launchBillingFlow_clientDisconnected() { // Prepare the launch call after disconnecting the client - MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); - methodChannelHandler.onMethodCall(disconnectCall, mock(Result.class)); + methodChannelHandler.endConnection(); String productId = "foo"; String accountId = "account"; HashMap arguments = new HashMap<>(); @@ -807,8 +801,7 @@ public void launchBillingFlow_oldProductNotFound() { @Test public void queryPurchases_clientDisconnected() { - // Prepare the launch call after disconnecting the client - methodChannelHandler.onMethodCall(new MethodCall(END_CONNECTION, null), mock(Result.class)); + methodChannelHandler.endConnection(); HashMap arguments = new HashMap<>(); arguments.put("type", BillingClient.ProductType.INAPP); @@ -901,8 +894,7 @@ public void queryPurchaseHistoryAsync() { @Test public void queryPurchaseHistoryAsync_clientDisconnected() { - // Prepare the launch call after disconnecting the client - methodChannelHandler.onMethodCall(new MethodCall(END_CONNECTION, null), mock(Result.class)); + methodChannelHandler.endConnection(); HashMap arguments = new HashMap<>(); arguments.put("type", BillingClient.ProductType.INAPP); 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 822babc9087..e53915e00fc 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 @@ -133,7 +133,7 @@ class BillingClient { /// /// This triggers the destruction of the `BillingClient` instance in Java. Future endConnection() async { - return channel.invokeMethod('BillingClient#endConnection()'); + return _hostApi.endConnection(); } /// Returns a list of [ProductDetailsResponseWrapper]s that have diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart index e41259fb5a5..4db198de568 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -18,7 +18,7 @@ PlatformException _createConnectionError(String channelName) { ); } -/// Pigeon version of BillingChoiceMode. +/// Pigeon version of billing_client_wrapper.dart's BillingChoiceMode. enum PlatformBillingChoiceMode { /// Billing through google play. /// @@ -29,7 +29,7 @@ enum PlatformBillingChoiceMode { alternativeBillingOnly, } -/// Pigeon version of BillingResult. +/// Pigeon version of Java BillingResult. class PlatformBillingResult { PlatformBillingResult({ required this.responseCode, @@ -150,4 +150,29 @@ class InAppPurchaseApi { return (__pigeon_replyList[0] as PlatformBillingResult?)!; } } + + /// Wraps BillingClient#endConnection(BillingClientStateListener). + Future endConnection() async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.endConnection'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send(null) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else { + return; + } + } } diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart index 0558e0a87f6..c8fc05ddd93 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -40,4 +40,7 @@ abstract class InAppPurchaseApi { @async PlatformBillingResult startConnection( int callbackHandle, PlatformBillingChoiceMode billingMode); + + /// Wraps BillingClient#endConnection(BillingClientStateListener). + void endConnection(); } 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 52d663aca2c..819bfa69f90 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 @@ -8,39 +8,28 @@ 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 'package:in_app_purchase_android/src/messages.g.dart'; import 'package:mockito/mockito.dart'; -import '../stub_in_app_purchase_platform.dart'; import 'billing_client_wrapper_test.mocks.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); late MockInAppPurchaseApi mockApi; late BillingClientManager manager; - const String endConnectionCall = 'BillingClient#endConnection()'; const String onBillingServiceDisconnectedCallback = 'BillingClientStateListener#onBillingServiceDisconnected()'; - setUpAll(() => TestDefaultBinaryMessengerBinding - .instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, stubPlatform.fakeMethodCallHandler)); - setUp(() { WidgetsFlutterBinding.ensureInitialized(); - stubPlatform.addResponse(name: endConnectionCall); mockApi = MockInAppPurchaseApi(); manager = BillingClientManager( billingClientFactory: (PurchasesUpdatedListener listener) => BillingClient(listener, api: mockApi)); }); - tearDown(() => stubPlatform.reset()); - group('BillingClientWrapper', () { test('connects on initialization', () { verify(mockApi.startConnection(any, any)).called(1); @@ -89,9 +78,8 @@ void main() { await manager.reconnectWithBillingChoiceMode( BillingChoiceMode.alternativeBillingOnly); // Verify that connection was ended. - expect(stubPlatform.countPreviousCalls(endConnectionCall), equals(1)); + verify(mockApi.endConnection()).called(1); - stubPlatform.reset(); clearInteractions(mockApi); /// Fake the disconnect that we would expect from a endConnectionCall. @@ -132,7 +120,7 @@ void main() { clearInteractions(mockApi); manager.dispose(); verifyNever(mockApi.startConnection(any, any)); - expect(stubPlatform.countPreviousCalls(endConnectionCall), equals(1)); + verify(mockApi.endConnection()).called(1); }); }); } diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index 1d26e914eec..21855d7e871 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -117,11 +117,9 @@ void main() { }); test('endConnection', () async { - const String endConnectionName = 'BillingClient#endConnection()'; - expect(stubPlatform.countPreviousCalls(endConnectionName), equals(0)); - stubPlatform.addResponse(name: endConnectionName); + verifyNever(mockApi.endConnection()); await billingClient.endConnection(); - expect(stubPlatform.countPreviousCalls(endConnectionName), equals(1)); + verify(mockApi.endConnection()).called(1); }); group('queryProductDetails', () { 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 6d03452b3ca..a3f76e58ec0 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 @@ -23,7 +23,6 @@ void main() { final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); late MockInAppPurchaseApi mockApi; late InAppPurchaseAndroidPlatformAddition iapAndroidPlatformAddition; - const String endConnectionCall = 'BillingClient#endConnection()'; const String onBillingServiceDisconnectedCallback = 'BillingClientStateListener#onBillingServiceDisconnected()'; late BillingClientManager manager; @@ -35,7 +34,6 @@ void main() { setUp(() { widgets.WidgetsFlutterBinding.ensureInitialized(); - stubPlatform.addResponse(name: endConnectionCall); mockApi = MockInAppPurchaseApi(); manager = BillingClientManager( billingClientFactory: (PurchasesUpdatedListener listener) => @@ -86,7 +84,6 @@ void main() { test('setAlternativeBillingOnlyState', () async { stubPlatform.reset(); clearInteractions(mockApi); - stubPlatform.addResponse(name: endConnectionCall); await iapAndroidPlatformAddition .setBillingChoice(BillingChoiceMode.alternativeBillingOnly); @@ -106,7 +103,6 @@ void main() { test('setPlayBillingState', () async { stubPlatform.reset(); clearInteractions(mockApi); - stubPlatform.addResponse(name: endConnectionCall); await iapAndroidPlatformAddition .setBillingChoice(BillingChoiceMode.playBillingOnly); 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 282f6f74d86..86375cf5a3a 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,7 +24,6 @@ void main() { final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); late MockInAppPurchaseApi mockApi; late InAppPurchaseAndroidPlatform iapAndroidPlatform; - const String endConnectionCall = 'BillingClient#endConnection()'; const String acknowledgePurchaseCall = 'BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; const String onBillingServiceDisconnectedCallback = @@ -37,7 +36,6 @@ void main() { setUp(() { widgets.WidgetsFlutterBinding.ensureInitialized(); - stubPlatform.addResponse(name: endConnectionCall); mockApi = MockInAppPurchaseApi(); iapAndroidPlatform = InAppPurchaseAndroidPlatform( From 9e9dcafe1d415f52b995adf079a93b453ee38251 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 4 Mar 2024 15:55:51 -0500 Subject: [PATCH 05/25] Convert isAlternativeBillingOnlyAvailable --- .../plugins/inapppurchase/Messages.java | 31 ++++++++++++++ .../inapppurchase/MethodCallHandlerImpl.java | 15 +++---- .../inapppurchase/MethodCallHandlerTest.java | 42 ++++++++++--------- .../billing_client_wrapper.dart | 14 +------ .../lib/src/messages.g.dart | 30 +++++++++++++ .../pigeons/messages.dart | 4 ++ .../billing_client_wrapper_test.dart | 19 +++------ ...rchase_android_platform_addition_test.dart | 7 ++-- 8 files changed, 102 insertions(+), 60 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java index 1545ebc2568..3bffe68b8b3 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -226,6 +226,8 @@ void startConnection( @NonNull Result result); /** Wraps BillingClient#endConnection(BillingClientStateListener). */ void endConnection(); + /** Wraps BillingClient#isAlternativeBillingOnlyAvailableAsync(). */ + void isAlternativeBillingOnlyAvailable(@NonNull Result result); /** The codec used by InAppPurchaseApi. */ static @NonNull MessageCodec getCodec() { @@ -317,6 +319,35 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.isAlternativeBillingOnlyAvailable", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success(PlatformBillingResult result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.isAlternativeBillingOnlyAvailable(resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } } } } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index 158e1e27473..d155ab5d5d6 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -67,8 +67,6 @@ static final class MethodNames { static final String IS_FEATURE_SUPPORTED = "BillingClient#isFeatureSupported(String)"; static final String GET_CONNECTION_STATE = "BillingClient#getConnectionState()"; static final String GET_BILLING_CONFIG = "BillingClient#getBillingConfig()"; - static final String IS_ALTERNATIVE_BILLING_ONLY_AVAILABLE = - "BillingClient#isAlternativeBillingOnlyAvailable()"; static final String CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS = "BillingClient#createAlternativeBillingOnlyReportingDetails()"; static final String SHOW_ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG = @@ -190,9 +188,6 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result case MethodNames.GET_BILLING_CONFIG: getBillingConfig(result); break; - case MethodNames.IS_ALTERNATIVE_BILLING_ONLY_AVAILABLE: - isAlternativeBillingOnlyAvailable(result); - break; case MethodNames.CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS: createAlternativeBillingOnlyReportingDetails(result); break; @@ -231,13 +226,13 @@ private void createAlternativeBillingOnlyReportingDetails(final MethodChannel.Re })); } - private void isAlternativeBillingOnlyAvailable(final MethodChannel.Result result) { - if (billingClientError(result)) { - return; - } + @Override + public void isAlternativeBillingOnlyAvailable( + @NonNull Messages.Result result) { + validateBillingClient(); billingClient.isAlternativeBillingOnlyAvailableAsync( billingResult -> { - result.success(fromBillingResult(billingResult)); + result.success(pigeonBillingResultFromBillingResult(billingResult)); }); } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index f40ec1e552e..3d2c3776edb 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -9,7 +9,6 @@ import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.CONSUME_PURCHASE_ASYNC; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.GET_BILLING_CONFIG; -import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.IS_ALTERNATIVE_BILLING_ONLY_AVAILABLE; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.IS_FEATURE_SUPPORTED; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.LAUNCH_BILLING_FLOW; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.ON_DISCONNECT; @@ -101,7 +100,7 @@ public class MethodCallHandlerTest { @Mock BillingClient mockBillingClient; @Mock MethodChannel mockMethodChannel; @Spy Result result; - @Spy Messages.Result connectionResult; + @Spy Messages.Result platformBillingResult; @Mock Activity activity; @Mock Context context; @Mock ActivityPluginBinding mockActivityPluginBinding; @@ -157,7 +156,7 @@ public void isReady_clientDisconnected() { public void startConnection() { ArgumentCaptor captor = mockStartConnection(PlatformBillingChoiceMode.PLAY_BILLING_ONLY); - verify(connectionResult, never()).success(any()); + verify(platformBillingResult, never()).success(any()); verify(factory, times(1)) .createBillingClient( context, mockMethodChannel, PlatformBillingChoiceMode.PLAY_BILLING_ONLY); @@ -171,7 +170,7 @@ public void startConnection() { ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(PlatformBillingResult.class); - verify(connectionResult, times(1)).success(resultCaptor.capture()); + verify(platformBillingResult, times(1)).success(resultCaptor.capture()); assertEquals( resultCaptor.getValue().getResponseCode().longValue(), billingResult.getResponseCode()); assertEquals(resultCaptor.getValue().getDebugMessage(), billingResult.getDebugMessage()); @@ -181,7 +180,7 @@ public void startConnection() { public void startConnectionAlternativeBillingOnly() { ArgumentCaptor captor = mockStartConnection(PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY); - verify(connectionResult, never()).success(any()); + verify(platformBillingResult, never()).success(any()); verify(factory, times(1)) .createBillingClient( context, mockMethodChannel, PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY); @@ -195,7 +194,7 @@ public void startConnectionAlternativeBillingOnly() { ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(PlatformBillingResult.class); - verify(connectionResult, times(1)).success(resultCaptor.capture()); + verify(platformBillingResult, times(1)).success(resultCaptor.capture()); assertEquals( resultCaptor.getValue().getResponseCode().longValue(), billingResult.getResponseCode()); assertEquals(resultCaptor.getValue().getDebugMessage(), billingResult.getDebugMessage()); @@ -208,8 +207,8 @@ public void startConnection_multipleCalls() { doNothing().when(mockBillingClient).startConnection(captor.capture()); methodChannelHandler.startConnection( - 1L, PlatformBillingChoiceMode.PLAY_BILLING_ONLY, connectionResult); - verify(connectionResult, never()).success(any()); + 1L, PlatformBillingChoiceMode.PLAY_BILLING_ONLY, platformBillingResult); + verify(platformBillingResult, never()).success(any()); BillingResult billingResult1 = BillingResult.newBuilder() .setResponseCode(100) @@ -232,11 +231,11 @@ public void startConnection_multipleCalls() { ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(PlatformBillingResult.class); - verify(connectionResult, times(1)).success(resultCaptor.capture()); + verify(platformBillingResult, times(1)).success(resultCaptor.capture()); assertEquals( resultCaptor.getValue().getResponseCode().longValue(), billingResult1.getResponseCode()); assertEquals(resultCaptor.getValue().getDebugMessage(), billingResult1.getDebugMessage()); - verify(connectionResult, times(1)).success(any()); + verify(platformBillingResult, times(1)).success(any()); } @Test @@ -320,7 +319,6 @@ public void isAlternativeBillingOnlyAvailableSuccess() { mockStartConnection(); ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(AlternativeBillingOnlyAvailabilityListener.class); - MethodCall billingCall = new MethodCall(IS_ALTERNATIVE_BILLING_ONLY_AVAILABLE, null); BillingResult billingResult = BillingResult.newBuilder() .setResponseCode(BillingClient.BillingResponseCode.OK) @@ -332,20 +330,24 @@ public void isAlternativeBillingOnlyAvailableSuccess() { .when(mockBillingClient) .isAlternativeBillingOnlyAvailableAsync(listenerCaptor.capture()); - methodChannelHandler.onMethodCall(billingCall, result); + methodChannelHandler.isAlternativeBillingOnlyAvailable(platformBillingResult); listenerCaptor.getValue().onAlternativeBillingOnlyAvailabilityResponse(billingResult); - verify(result, times(1)).success(fromBillingResult(billingResult)); + ArgumentCaptor resultCaptor = + ArgumentCaptor.forClass(PlatformBillingResult.class); + verify(platformBillingResult, times(1)).success(resultCaptor.capture()); + assertEquals( + resultCaptor.getValue().getResponseCode().longValue(), billingResult.getResponseCode()); + assertEquals(resultCaptor.getValue().getDebugMessage(), billingResult.getDebugMessage()); } @Test public void isAlternativeBillingOnlyAvailable_serviceDisconnected() { - MethodCall billingCall = new MethodCall(IS_ALTERNATIVE_BILLING_ONLY_AVAILABLE, null); - methodChannelHandler.onMethodCall(billingCall, mock(Result.class)); - - methodChannelHandler.onMethodCall(billingCall, result); - - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); + Messages.FlutterError exception = + assertThrows( + Messages.FlutterError.class, + () -> methodChannelHandler.isAlternativeBillingOnlyAvailable(platformBillingResult)); + assertEquals("UNAVAILABLE", exception.code); } @Test @@ -1052,7 +1054,7 @@ private ArgumentCaptor mockStartConnection( ArgumentCaptor.forClass(BillingClientStateListener.class); doNothing().when(mockBillingClient).startConnection(captor.capture()); - methodChannelHandler.startConnection(1L, billingChoiceMode, connectionResult); + methodChannelHandler.startConnection(1L, billingChoiceMode, platformBillingResult); return captor; } 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 e53915e00fc..430df3f514f 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 @@ -342,20 +342,10 @@ class BillingClient { {}); } - /// isAlternativeBillingOnlyAvailable method channel string identifier. - // - // Must match the value of IS_ALTERNATIVE_BILLING_ONLY_AVAILABLE in - // ../../../android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java - @visibleForTesting - static const String isAlternativeBillingOnlyAvailableMethodString = - 'BillingClient#isAlternativeBillingOnlyAvailable()'; - /// Checks if "AlterntitiveBillingOnly" feature is available. Future isAlternativeBillingOnlyAvailable() async { - return BillingResultWrapper.fromJson( - (await channel.invokeMapMethod( - isAlternativeBillingOnlyAvailableMethodString)) ?? - {}); + return resultWrapperFromPlatform( + await _hostApi.isAlternativeBillingOnlyAvailable()); } /// showAlternativeBillingOnlyInformationDialog method channel string identifier. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart index 4db198de568..bbf3ea2c5ae 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -175,4 +175,34 @@ class InAppPurchaseApi { return; } } + + /// Wraps BillingClient#isAlternativeBillingOnlyAvailableAsync(). + Future isAlternativeBillingOnlyAvailable() async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.isAlternativeBillingOnlyAvailable'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send(null) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as PlatformBillingResult?)!; + } + } } diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart index c8fc05ddd93..b8aa30d0247 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -43,4 +43,8 @@ abstract class InAppPurchaseApi { /// Wraps BillingClient#endConnection(BillingClientStateListener). void endConnection(); + + /// Wraps BillingClient#isAlternativeBillingOnlyAvailableAsync(). + @async + PlatformBillingResult isAlternativeBillingOnlyAvailable(); } diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index 21855d7e871..adbc1d5d066 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -667,24 +667,15 @@ void main() { group('isAlternativeBillingOnlyAvailable', () { test('returns object', () async { - const BillingResultWrapper expected = - BillingResultWrapper(responseCode: BillingResponse.ok); - stubPlatform.addResponse( - name: BillingClient.isAlternativeBillingOnlyAvailableMethodString, - value: buildBillingResultMap(expected)); + const BillingResultWrapper expected = BillingResultWrapper( + responseCode: BillingResponse.ok, debugMessage: 'message'); + when(mockApi.isAlternativeBillingOnlyAvailable()).thenAnswer((_) async => + PlatformBillingResult( + responseCode: 0, debugMessage: expected.debugMessage!)); final BillingResultWrapper result = await billingClient.isAlternativeBillingOnlyAvailable(); expect(result, expected); }); - - test('handles method channel returning null', () async { - stubPlatform.addResponse( - name: BillingClient.isAlternativeBillingOnlyAvailableMethodString, - ); - final BillingResultWrapper result = - await billingClient.isAlternativeBillingOnlyAvailable(); - expect(result.responseCode, BillingResponse.error); - }); }); group('createAlternativeBillingOnlyReportingDetails', () { 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 a3f76e58ec0..545bf5d58eb 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 @@ -123,11 +123,10 @@ void main() { test('isAlternativeBillingOnlyAvailable success', () async { const BillingResultWrapper expected = BillingResultWrapper( responseCode: BillingResponse.ok, debugMessage: 'dummy message'); + when(mockApi.isAlternativeBillingOnlyAvailable()).thenAnswer((_) async => + PlatformBillingResult( + responseCode: 0, debugMessage: expected.debugMessage!)); - stubPlatform.addResponse( - name: BillingClient.isAlternativeBillingOnlyAvailableMethodString, - value: buildBillingResultMap(expected), - ); final BillingResultWrapper result = await iapAndroidPlatformAddition.isAlternativeBillingOnlyAvailable(); From 511ce2241d248643faf8e156e169852eb9e1aac7 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 4 Mar 2024 16:08:25 -0500 Subject: [PATCH 06/25] Minor warning fixes from IDE --- .../inapppurchase/MethodCallHandlerImpl.java | 37 ++++++++----------- .../inapppurchase/MethodCallHandlerTest.java | 33 ++++++++--------- 2 files changed, 31 insertions(+), 39 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index d155ab5d5d6..a30c4495d79 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -118,22 +118,22 @@ void setActivity(@Nullable Activity activity) { } @Override - public void onActivityCreated(Activity activity, Bundle savedInstanceState) {} + public void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) {} @Override - public void onActivityStarted(Activity activity) {} + public void onActivityStarted(@NonNull Activity activity) {} @Override - public void onActivityResumed(Activity activity) {} + public void onActivityResumed(@NonNull Activity activity) {} @Override - public void onActivityPaused(Activity activity) {} + public void onActivityPaused(@NonNull Activity activity) {} @Override - public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} + public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {} @Override - public void onActivityDestroyed(Activity activity) { + public void onActivityDestroyed(@NonNull Activity activity) { if (this.activity == activity && this.applicationContext != null) { ((Application) this.applicationContext).unregisterActivityLifecycleCallbacks(this); endBillingClientConnection(); @@ -141,7 +141,7 @@ public void onActivityDestroyed(Activity activity) { } @Override - public void onActivityStopped(Activity activity) {} + public void onActivityStopped(@NonNull Activity activity) {} void onDetachedFromActivity() { endBillingClientConnection(); @@ -208,10 +208,7 @@ private void showAlternativeBillingOnlyInformationDialog(final MethodChannel.Res return; } billingClient.showAlternativeBillingOnlyInformationDialog( - activity, - billingResult -> { - result.success(fromBillingResult(billingResult)); - }); + activity, billingResult -> result.success(fromBillingResult(billingResult))); } private void createAlternativeBillingOnlyReportingDetails(final MethodChannel.Result result) { @@ -219,11 +216,10 @@ private void createAlternativeBillingOnlyReportingDetails(final MethodChannel.Re return; } billingClient.createAlternativeBillingOnlyReportingDetailsAsync( - ((billingResult, alternativeBillingOnlyReportingDetails) -> { - result.success( - fromAlternativeBillingOnlyReportingDetails( - billingResult, alternativeBillingOnlyReportingDetails)); - })); + ((billingResult, alternativeBillingOnlyReportingDetails) -> + result.success( + fromAlternativeBillingOnlyReportingDetails( + billingResult, alternativeBillingOnlyReportingDetails)))); } @Override @@ -231,9 +227,7 @@ public void isAlternativeBillingOnlyAvailable( @NonNull Messages.Result result) { validateBillingClient(); billingClient.isAlternativeBillingOnlyAvailableAsync( - billingResult -> { - result.success(pigeonBillingResultFromBillingResult(billingResult)); - }); + billingResult -> result.success(pigeonBillingResultFromBillingResult(billingResult))); } private void getBillingConfig(final MethodChannel.Result result) { @@ -242,9 +236,8 @@ private void getBillingConfig(final MethodChannel.Result result) { } billingClient.getBillingConfigAsync( GetBillingConfigParams.newBuilder().build(), - (billingResult, billingConfig) -> { - result.success(fromBillingConfig(billingResult, billingConfig)); - }); + (billingResult, billingConfig) -> + result.success(fromBillingConfig(billingResult, billingConfig))); } @Override diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index 3d2c3776edb..6726b4a6ddc 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -85,6 +85,7 @@ import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -95,6 +96,7 @@ import org.mockito.stubbing.Answer; public class MethodCallHandlerTest { + private AutoCloseable openMocks; private MethodCallHandlerImpl methodChannelHandler; @Mock BillingClientFactory factory; @Mock BillingClient mockBillingClient; @@ -108,7 +110,7 @@ public class MethodCallHandlerTest { @Before public void setUp() { - MockitoAnnotations.openMocks(this); + openMocks = MockitoAnnotations.openMocks(this); // Use the same client no matter if alternative billing is enabled or not. when(factory.createBillingClient( context, mockMethodChannel, PlatformBillingChoiceMode.PLAY_BILLING_ONLY)) @@ -120,6 +122,11 @@ public void setUp() { when(mockActivityPluginBinding.getActivity()).thenReturn(activity); } + @After + public void tearDown() throws Exception { + openMocks.close(); + } + @Test public void invalidMethod() { MethodCall call = new MethodCall("invalid", null); @@ -324,7 +331,6 @@ public void isAlternativeBillingOnlyAvailableSuccess() { .setResponseCode(BillingClient.BillingResponseCode.OK) .setDebugMessage("dummy debug message") .build(); - final HashMap expectedResult = fromBillingResult(billingResult); doNothing() .when(mockBillingClient) @@ -442,8 +448,7 @@ public void queryProductDetailsAsync() { .queryProductDetailsAsync(paramCaptor.capture(), listenerCaptor.capture()); // Assert that we handed result BillingClient's response - int responseCode = 200; - List productDetailsResponse = asList(buildProductDetails("foo")); + List productDetailsResponse = singletonList(buildProductDetails("foo")); BillingResult billingResult = BillingResult.newBuilder() .setResponseCode(100) @@ -502,7 +507,6 @@ public void launchBillingFlow_null_AccountId_do_not_crash() { ArgumentCaptor billingFlowParamsCaptor = ArgumentCaptor.forClass(BillingFlowParams.class); verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); - BillingFlowParams params = billingFlowParamsCaptor.getValue(); // Verify we pass the response code to result verify(result, never()).error(any(), any(), any()); @@ -534,7 +538,7 @@ public void launchBillingFlow_ok_null_OldProduct() { ArgumentCaptor billingFlowParamsCaptor = ArgumentCaptor.forClass(BillingFlowParams.class); verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); - BillingFlowParams params = billingFlowParamsCaptor.getValue(); + // Verify we pass the response code to result verify(result, never()).error(any(), any(), any()); verify(result, times(1)).success(fromBillingResult(billingResult)); @@ -585,7 +589,6 @@ public void launchBillingFlow_ok_oldProduct() { ArgumentCaptor billingFlowParamsCaptor = ArgumentCaptor.forClass(BillingFlowParams.class); verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); - BillingFlowParams params = billingFlowParamsCaptor.getValue(); // Verify we pass the response code to result verify(result, never()).error(any(), any(), any()); @@ -616,7 +619,6 @@ public void launchBillingFlow_ok_AccountId() { ArgumentCaptor billingFlowParamsCaptor = ArgumentCaptor.forClass(BillingFlowParams.class); verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); - BillingFlowParams params = billingFlowParamsCaptor.getValue(); // Verify we pass the response code to result verify(result, never()).error(any(), any(), any()); @@ -657,7 +659,6 @@ public void launchBillingFlow_ok_Proration() { ArgumentCaptor billingFlowParamsCaptor = ArgumentCaptor.forClass(BillingFlowParams.class); verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); - BillingFlowParams params = billingFlowParamsCaptor.getValue(); // Verify we pass the response code to result verify(result, never()).error(any(), any(), any()); @@ -674,13 +675,12 @@ public void launchBillingFlow_ok_Proration_with_null_OldProduct() { String productId = "foo"; String accountId = "account"; String queryOldProductId = "oldFoo"; - String oldProductId = null; int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE; queryForProducts(unmodifiableList(asList(productId, queryOldProductId))); HashMap arguments = new HashMap<>(); arguments.put("product", productId); arguments.put("accountId", accountId); - arguments.put("oldProduct", oldProductId); + arguments.put("oldProduct", null); arguments.put("prorationMode", prorationMode); MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); @@ -736,7 +736,6 @@ public void launchBillingFlow_ok_Full() { ArgumentCaptor billingFlowParamsCaptor = ArgumentCaptor.forClass(BillingFlowParams.class); verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); - BillingFlowParams params = billingFlowParamsCaptor.getValue(); // Verify we pass the response code to result verify(result, never()).error(any(), any(), any()); @@ -839,7 +838,7 @@ public void queryPurchases_returns_success() throws Exception { .setDebugMessage("hello message"); purchasesResponseListenerArgumentCaptor .getValue() - .onQueryPurchasesResponse(resultBuilder.build(), new ArrayList()); + .onQueryPurchasesResponse(resultBuilder.build(), new ArrayList<>()); return null; }) .when(mockBillingClient) @@ -874,7 +873,7 @@ public void queryPurchaseHistoryAsync() { .setResponseCode(100) .setDebugMessage("dummy debug message") .build(); - List purchasesList = asList(buildPurchaseHistoryRecord("foo")); + List purchasesList = singletonList(buildPurchaseHistoryRecord("foo")); HashMap arguments = new HashMap<>(); arguments.put("productType", BillingClient.ProductType.INAPP); ArgumentCaptor listenerCaptor = @@ -917,7 +916,7 @@ public void onPurchasesUpdatedListener() { .setResponseCode(100) .setDebugMessage("dummy debug message") .build(); - List purchasesList = asList(buildPurchase("foo")); + List purchasesList = singletonList(buildPurchase("foo")); doNothing() .when(mockMethodChannel) .invokeMethod(eq(ON_PURCHASES_UPDATED), resultCaptor.capture()); @@ -1036,7 +1035,7 @@ public void isFutureSupported_false() { /** * Call {@link MethodCallHandlerImpl#startConnection(Long, PlatformBillingChoiceMode, - * Messages.Result>)} with startup params. + * Messages.Result)} with startup params. * *

Defaults to play billing only which is the default. */ @@ -1046,7 +1045,7 @@ private ArgumentCaptor mockStartConnection() { /** * Call {@link MethodCallHandlerImpl#startConnection(Long, PlatformBillingChoiceMode, - * Messages.Result>)} with startup params. + * Messages.Result)} with startup params. */ private ArgumentCaptor mockStartConnection( PlatformBillingChoiceMode billingChoiceMode) { From 6bada4997abd04672f4765a042b4367366e00abe Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 4 Mar 2024 16:17:39 -0500 Subject: [PATCH 07/25] Remove unreachable code --- .../inapppurchase/MethodCallHandlerImpl.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index a30c4495d79..be4a40599f3 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -65,7 +65,6 @@ static final class MethodNames { static final String ACKNOWLEDGE_PURCHASE = "BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)"; static final String IS_FEATURE_SUPPORTED = "BillingClient#isFeatureSupported(String)"; - static final String GET_CONNECTION_STATE = "BillingClient#getConnectionState()"; static final String GET_BILLING_CONFIG = "BillingClient#getBillingConfig()"; static final String CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS = "BillingClient#createAlternativeBillingOnlyReportingDetails()"; @@ -182,9 +181,6 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result case MethodNames.IS_FEATURE_SUPPORTED: isFeatureSupported((String) call.argument("feature"), result); break; - case MethodNames.GET_CONNECTION_STATE: - getConnectionState(result); - break; case MethodNames.GET_BILLING_CONFIG: getBillingConfig(result); break; @@ -449,15 +445,6 @@ private void queryPurchaseHistoryAsync(String productType, final MethodChannel.R }); } - private void getConnectionState(final MethodChannel.Result result) { - if (billingClientError(result)) { - return; - } - final Map serialized = new HashMap<>(); - serialized.put("connectionState", billingClient.getConnectionState()); - result.success(serialized); - } - @Override public void startConnection( @NonNull Long handle, From 22a9434ac371dc7ecb3e4064698e38e315344ca0 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 4 Mar 2024 16:37:43 -0500 Subject: [PATCH 08/25] Convert showAlternativeBillingOnlyInformationDialog --- .../plugins/inapppurchase/Messages.java | 31 +++++++++++++ .../inapppurchase/MethodCallHandlerImpl.java | 19 +++----- .../inapppurchase/MethodCallHandlerTest.java | 43 ++++++++++++------- .../billing_client_wrapper.dart | 14 +----- .../lib/src/messages.g.dart | 31 +++++++++++++ .../pigeons/messages.dart | 4 ++ .../billing_client_wrapper_test.dart | 21 +++------ ...rchase_android_platform_addition_test.dart | 8 ++-- 8 files changed, 110 insertions(+), 61 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java index 3bffe68b8b3..a7488a6ce58 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -228,6 +228,8 @@ void startConnection( void endConnection(); /** Wraps BillingClient#isAlternativeBillingOnlyAvailableAsync(). */ void isAlternativeBillingOnlyAvailable(@NonNull Result result); + /** Wraps BillingClient#showAlternativeBillingOnlyInformationDialog(). */ + void showAlternativeBillingOnlyInformationDialog(@NonNull Result result); /** The codec used by InAppPurchaseApi. */ static @NonNull MessageCodec getCodec() { @@ -348,6 +350,35 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.showAlternativeBillingOnlyInformationDialog", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success(PlatformBillingResult result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.showAlternativeBillingOnlyInformationDialog(resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } } } } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index be4a40599f3..e3ba2fef31c 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -68,8 +68,6 @@ static final class MethodNames { static final String GET_BILLING_CONFIG = "BillingClient#getBillingConfig()"; static final String CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS = "BillingClient#createAlternativeBillingOnlyReportingDetails()"; - static final String SHOW_ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG = - "BillingClient#showAlternativeBillingOnlyInformationDialog()"; private MethodNames() {} } @@ -187,24 +185,21 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result case MethodNames.CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS: createAlternativeBillingOnlyReportingDetails(result); break; - case MethodNames.SHOW_ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG: - showAlternativeBillingOnlyInformationDialog(result); - break; default: result.notImplemented(); } } - private void showAlternativeBillingOnlyInformationDialog(final MethodChannel.Result result) { - if (billingClientError(result)) { - return; - } + @Override + public void showAlternativeBillingOnlyInformationDialog( + @NonNull Messages.Result result) { + validateBillingClient(); if (activity == null) { - result.error(ACTIVITY_UNAVAILABLE, "Not attempting to show dialog", null); - return; + throw new FlutterError(ACTIVITY_UNAVAILABLE, "Not attempting to show dialog", null); } billingClient.showAlternativeBillingOnlyInformationDialog( - activity, billingResult -> result.success(fromBillingResult(billingResult))); + activity, + billingResult -> result.success(pigeonBillingResultFromBillingResult(billingResult))); } private void createAlternativeBillingOnlyReportingDetails(final MethodChannel.Result result) { diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index 6726b4a6ddc..bfce7f2029f 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -15,7 +15,6 @@ import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PRODUCT_DETAILS; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PURCHASES_ASYNC; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC; -import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.SHOW_ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG; import static io.flutter.plugins.inapppurchase.PluginPurchaseListener.ON_PURCHASES_UPDATED; import static io.flutter.plugins.inapppurchase.Translator.fromAlternativeBillingOnlyReportingDetails; import static io.flutter.plugins.inapppurchase.Translator.fromBillingConfig; @@ -83,6 +82,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.After; @@ -157,6 +157,7 @@ public void isReady_clientDisconnected() { Messages.FlutterError exception = assertThrows(Messages.FlutterError.class, () -> methodChannelHandler.isReady()); assertEquals("UNAVAILABLE", exception.code); + assertTrue(Objects.requireNonNull(exception.getMessage()).contains("BillingClient")); } @Test @@ -354,6 +355,7 @@ public void isAlternativeBillingOnlyAvailable_serviceDisconnected() { Messages.FlutterError.class, () -> methodChannelHandler.isAlternativeBillingOnlyAvailable(platformBillingResult)); assertEquals("UNAVAILABLE", exception.code); + assertTrue(Objects.requireNonNull(exception.getMessage()).contains("BillingClient")); } @Test @@ -361,8 +363,6 @@ public void showAlternativeBillingOnlyInformationDialogSuccess() { mockStartConnection(); ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(AlternativeBillingOnlyInformationDialogListener.class); - MethodCall showDialogCall = - new MethodCall(SHOW_ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG, null); BillingResult billingResult = BillingResult.newBuilder() .setResponseCode(BillingResponseCode.OK) @@ -373,32 +373,43 @@ public void showAlternativeBillingOnlyInformationDialogSuccess() { eq(activity), listenerCaptor.capture())) .thenReturn(billingResult); - methodChannelHandler.onMethodCall(showDialogCall, result); + methodChannelHandler.showAlternativeBillingOnlyInformationDialog(platformBillingResult); listenerCaptor.getValue().onAlternativeBillingOnlyInformationDialogResponse(billingResult); - verify(result, times(1)).success(fromBillingResult(billingResult)); + ArgumentCaptor resultCaptor = + ArgumentCaptor.forClass(PlatformBillingResult.class); + verify(platformBillingResult, times(1)).success(resultCaptor.capture()); + assertEquals( + resultCaptor.getValue().getResponseCode().longValue(), billingResult.getResponseCode()); + assertEquals(resultCaptor.getValue().getDebugMessage(), billingResult.getDebugMessage()); } @Test public void showAlternativeBillingOnlyInformationDialog_serviceDisconnected() { - MethodCall billingCall = new MethodCall(SHOW_ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG, null); - - methodChannelHandler.onMethodCall(billingCall, result); - - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); + Messages.FlutterError exception = + assertThrows( + Messages.FlutterError.class, + () -> + methodChannelHandler.showAlternativeBillingOnlyInformationDialog( + platformBillingResult)); + assertEquals("UNAVAILABLE", exception.code); + assertTrue(Objects.requireNonNull(exception.getMessage()).contains("BillingClient")); } @Test public void showAlternativeBillingOnlyInformationDialog_NullActivity() { mockStartConnection(); - MethodCall showDialogCall = - new MethodCall(SHOW_ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG, null); - methodChannelHandler.setActivity(null); - methodChannelHandler.onMethodCall(showDialogCall, result); - verify(result) - .error(contains(ACTIVITY_UNAVAILABLE), contains("Not attempting to show dialog"), any()); + Messages.FlutterError exception = + assertThrows( + Messages.FlutterError.class, + () -> + methodChannelHandler.showAlternativeBillingOnlyInformationDialog( + platformBillingResult)); + assertEquals(ACTIVITY_UNAVAILABLE, exception.code); + assertTrue( + Objects.requireNonNull(exception.getMessage()).contains("Not attempting to show dialog")); } @Test 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 430df3f514f..390d77582e8 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 @@ -348,21 +348,11 @@ class BillingClient { await _hostApi.isAlternativeBillingOnlyAvailable()); } - /// showAlternativeBillingOnlyInformationDialog method channel string identifier. - // - // Must match the value of SHOW_ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG in - // ../../../android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java - @visibleForTesting - static const String showAlternativeBillingOnlyInformationDialogMethodString = - 'BillingClient#showAlternativeBillingOnlyInformationDialog()'; - /// Shows the alternative billing only information dialog on top of the calling app. Future showAlternativeBillingOnlyInformationDialog() async { - return BillingResultWrapper.fromJson( - (await channel.invokeMapMethod( - showAlternativeBillingOnlyInformationDialogMethodString)) ?? - {}); + return resultWrapperFromPlatform( + await _hostApi.showAlternativeBillingOnlyInformationDialog()); } /// createAlternativeBillingOnlyReportingDetails method channel string identifier. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart index bbf3ea2c5ae..17cd91b7a5a 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -205,4 +205,35 @@ class InAppPurchaseApi { return (__pigeon_replyList[0] as PlatformBillingResult?)!; } } + + /// Wraps BillingClient#showAlternativeBillingOnlyInformationDialog(). + Future + showAlternativeBillingOnlyInformationDialog() async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.showAlternativeBillingOnlyInformationDialog'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send(null) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as PlatformBillingResult?)!; + } + } } diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart index b8aa30d0247..0e1a8540bd4 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -47,4 +47,8 @@ abstract class InAppPurchaseApi { /// Wraps BillingClient#isAlternativeBillingOnlyAvailableAsync(). @async PlatformBillingResult isAlternativeBillingOnlyAvailable(); + + /// Wraps BillingClient#showAlternativeBillingOnlyInformationDialog(). + @async + PlatformBillingResult showAlternativeBillingOnlyInformationDialog(); } diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index adbc1d5d066..79c1e3a4839 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -707,26 +707,15 @@ void main() { group('showAlternativeBillingOnlyInformationDialog', () { test('returns object', () async { - const BillingResultWrapper expected = - BillingResultWrapper(responseCode: BillingResponse.ok); - stubPlatform.addResponse( - name: BillingClient - .showAlternativeBillingOnlyInformationDialogMethodString, - value: buildBillingResultMap(expected)); + const BillingResultWrapper expected = BillingResultWrapper( + responseCode: BillingResponse.ok, debugMessage: 'message'); + when(mockApi.showAlternativeBillingOnlyInformationDialog()).thenAnswer( + (_) async => PlatformBillingResult( + responseCode: 0, debugMessage: expected.debugMessage!)); final BillingResultWrapper result = await billingClient.showAlternativeBillingOnlyInformationDialog(); expect(result, expected); }); - - test('handles method channel returning null', () async { - stubPlatform.addResponse( - name: BillingClient - .showAlternativeBillingOnlyInformationDialogMethodString, - ); - final BillingResultWrapper result = - await billingClient.showAlternativeBillingOnlyInformationDialog(); - expect(result.responseCode, BillingResponse.error); - }); }); } 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 545bf5d58eb..9b762484a47 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 @@ -139,11 +139,9 @@ void main() { const BillingResultWrapper expected = BillingResultWrapper( responseCode: BillingResponse.ok, debugMessage: 'dummy message'); - stubPlatform.addResponse( - name: BillingClient - .showAlternativeBillingOnlyInformationDialogMethodString, - value: buildBillingResultMap(expected), - ); + when(mockApi.showAlternativeBillingOnlyInformationDialog()).thenAnswer( + (_) async => PlatformBillingResult( + responseCode: 0, debugMessage: expected.debugMessage!)); final BillingResultWrapper result = await iapAndroidPlatformAddition.isAlternativeBillingOnlyAvailable(); From f3b693877a65bc66f3c5426f9fc31c5a79a9d83f Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 5 Mar 2024 11:57:37 -0500 Subject: [PATCH 09/25] Convert consumeAsync, and fix issues caused by not having regenerated mocks in earlier conversions --- .../plugins/inapppurchase/Messages.java | 33 +++++ .../inapppurchase/MethodCallHandlerImpl.java | 18 +-- .../inapppurchase/MethodCallHandlerTest.java | 40 +++--- .../billing_client_wrapper.dart | 9 +- .../billing_response_wrapper.dart | 2 + .../lib/src/messages.g.dart | 30 +++++ .../pigeons/messages.dart | 4 + .../billing_client_manager_test.dart | 4 +- .../billing_client_wrapper_test.dart | 34 ++--- .../billing_client_wrapper_test.mocks.dart | 127 +++++++++++++++++- .../purchase_wrapper_test.dart | 14 ++ ...rchase_android_platform_addition_test.dart | 17 ++- ...in_app_purchase_android_platform_test.dart | 71 ++++------ 13 files changed, 280 insertions(+), 123 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java index a7488a6ce58..22963d1b40c 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -226,6 +226,8 @@ void startConnection( @NonNull Result result); /** Wraps BillingClient#endConnection(BillingClientStateListener). */ void endConnection(); + /** Wraps BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener). */ + void consumeAsync(@NonNull String purchaseToken, @NonNull Result result); /** Wraps BillingClient#isAlternativeBillingOnlyAvailableAsync(). */ void isAlternativeBillingOnlyAvailable(@NonNull Result result); /** Wraps BillingClient#showAlternativeBillingOnlyInformationDialog(). */ @@ -321,6 +323,37 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.consumeAsync", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String purchaseTokenArg = (String) args.get(0); + Result resultCallback = + new Result() { + public void success(PlatformBillingResult result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.consumeAsync(purchaseTokenArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } { BasicMessageChannel channel = new BasicMessageChannel<>( diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index e3ba2fef31c..e8437008537 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -60,8 +60,6 @@ static final class MethodNames { "BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)"; static final String QUERY_PURCHASE_HISTORY_ASYNC = "BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener)"; - static final String CONSUME_PURCHASE_ASYNC = - "BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener)"; static final String ACKNOWLEDGE_PURCHASE = "BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)"; static final String IS_FEATURE_SUPPORTED = "BillingClient#isFeatureSupported(String)"; @@ -170,9 +168,6 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result case MethodNames.QUERY_PURCHASE_HISTORY_ASYNC: queryPurchaseHistoryAsync((String) call.argument("productType"), result); break; - case MethodNames.CONSUME_PURCHASE_ASYNC: - consumeAsync((String) call.argument("purchaseToken"), result); - break; case MethodNames.ACKNOWLEDGE_PURCHASE: acknowledgePurchase((String) call.argument("purchaseToken"), result); break; @@ -388,16 +383,17 @@ private void setReplaceProrationMode( builder.setReplaceProrationMode(prorationMode); } - private void consumeAsync(String purchaseToken, final MethodChannel.Result result) { - if (billingClientError(result)) { - return; - } + @Override + public void consumeAsync( + @NonNull String purchaseToken, + @NonNull Messages.Result result) { + validateBillingClient(); ConsumeResponseListener listener = - (billingResult, outToken) -> result.success(fromBillingResult(billingResult)); + (billingResult, outToken) -> + result.success(pigeonBillingResultFromBillingResult(billingResult)); ConsumeParams.Builder paramsBuilder = ConsumeParams.newBuilder().setPurchaseToken(purchaseToken); - ConsumeParams params = paramsBuilder.build(); billingClient.consumeAsync(params, listener); diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index bfce7f2029f..6b11312f408 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -6,7 +6,6 @@ import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.ACTIVITY_UNAVAILABLE; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.ACKNOWLEDGE_PURCHASE; -import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.CONSUME_PURCHASE_ASYNC; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.GET_BILLING_CONFIG; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.IS_FEATURE_SUPPORTED; @@ -179,9 +178,7 @@ public void startConnection() { ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(PlatformBillingResult.class); verify(platformBillingResult, times(1)).success(resultCaptor.capture()); - assertEquals( - resultCaptor.getValue().getResponseCode().longValue(), billingResult.getResponseCode()); - assertEquals(resultCaptor.getValue().getDebugMessage(), billingResult.getDebugMessage()); + assertResultsMatch(resultCaptor.getValue(), billingResult); } @Test @@ -203,9 +200,7 @@ public void startConnectionAlternativeBillingOnly() { ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(PlatformBillingResult.class); verify(platformBillingResult, times(1)).success(resultCaptor.capture()); - assertEquals( - resultCaptor.getValue().getResponseCode().longValue(), billingResult.getResponseCode()); - assertEquals(resultCaptor.getValue().getDebugMessage(), billingResult.getDebugMessage()); + assertResultsMatch(resultCaptor.getValue(), billingResult); } @Test @@ -343,9 +338,7 @@ public void isAlternativeBillingOnlyAvailableSuccess() { ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(PlatformBillingResult.class); verify(platformBillingResult, times(1)).success(resultCaptor.capture()); - assertEquals( - resultCaptor.getValue().getResponseCode().longValue(), billingResult.getResponseCode()); - assertEquals(resultCaptor.getValue().getDebugMessage(), billingResult.getDebugMessage()); + assertResultsMatch(resultCaptor.getValue(), billingResult); } @Test @@ -379,9 +372,7 @@ public void showAlternativeBillingOnlyInformationDialogSuccess() { ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(PlatformBillingResult.class); verify(platformBillingResult, times(1)).success(resultCaptor.capture()); - assertEquals( - resultCaptor.getValue().getResponseCode().longValue(), billingResult.getResponseCode()); - assertEquals(resultCaptor.getValue().getDebugMessage(), billingResult.getDebugMessage()); + assertResultsMatch(resultCaptor.getValue(), billingResult); } @Test @@ -946,25 +937,25 @@ public void consumeAsync() { .setResponseCode(100) .setDebugMessage("dummy debug message") .build(); - HashMap arguments = new HashMap<>(); - arguments.put("purchaseToken", "mockToken"); - arguments.put("developerPayload", "mockPayload"); + final String token = "mockToken"; ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(ConsumeResponseListener.class); - methodChannelHandler.onMethodCall(new MethodCall(CONSUME_PURCHASE_ASYNC, arguments), result); + methodChannelHandler.consumeAsync(token, platformBillingResult); - ConsumeParams params = ConsumeParams.newBuilder().setPurchaseToken("mockToken").build(); + ConsumeParams params = ConsumeParams.newBuilder().setPurchaseToken(token).build(); // Verify we pass the data to result verify(mockBillingClient).consumeAsync(refEq(params), listenerCaptor.capture()); - listenerCaptor.getValue().onConsumeResponse(billingResult, "mockToken"); - verify(result).success(resultCaptor.capture()); + listenerCaptor.getValue().onConsumeResponse(billingResult, token); // Verify we pass the response code to result - verify(result, never()).error(any(), any(), any()); - verify(result, times(1)).success(fromBillingResult(billingResult)); + verify(platformBillingResult, never()).error(any()); + ArgumentCaptor resultCaptor = + ArgumentCaptor.forClass(PlatformBillingResult.class); + verify(platformBillingResult, times(1)).success(resultCaptor.capture()); + assertResultsMatch(resultCaptor.getValue(), billingResult); } @Test @@ -1147,4 +1138,9 @@ private PurchaseHistoryRecord buildPurchaseHistoryRecord(String purchaseToken) { when(purchase.getPurchaseToken()).thenReturn(purchaseToken); return purchase; } + + private void assertResultsMatch(PlatformBillingResult pigeonResult, BillingResult nativeResult) { + assertEquals(pigeonResult.getResponseCode().longValue(), nativeResult.getResponseCode()); + assertEquals(pigeonResult.getDebugMessage(), nativeResult.getDebugMessage()); + } } 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 390d77582e8..6551fa63069 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 @@ -280,13 +280,8 @@ class BillingClient { /// This wraps /// [`BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#consumeAsync(java.lang.String,%20com.android.billingclient.api.ConsumeResponseListener)) Future consumeAsync(String purchaseToken) async { - return BillingResultWrapper.fromJson((await channel.invokeMapMethod( - 'BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener)', - { - 'purchaseToken': purchaseToken, - })) ?? - {}); + return resultWrapperFromPlatform( + await _hostApi.consumeAsync(purchaseToken)); } /// Acknowledge an in-app purchase. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_response_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_response_wrapper.dart index 62887b00d43..583dd4ef716 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_response_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_response_wrapper.dart @@ -48,6 +48,8 @@ class BillingResultWrapper implements HasBillingResponse { /// /// Defaults to `null`. /// This message uses an en-US locale and should not be shown to users. + // TODO(stuartmorgan): Make this non-nullable, since the underlying native + // object's property is annotated as @NonNull. final String? debugMessage; @override diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart index 17cd91b7a5a..14b17933fbe 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -176,6 +176,36 @@ class InAppPurchaseApi { } } + /// Wraps BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener). + Future consumeAsync(String purchaseToken) async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.consumeAsync'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send([purchaseToken]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as PlatformBillingResult?)!; + } + } + /// Wraps BillingClient#isAlternativeBillingOnlyAvailableAsync(). Future isAlternativeBillingOnlyAvailable() async { const String __pigeon_channelName = diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart index 0e1a8540bd4..414dc5a1339 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -44,6 +44,10 @@ abstract class InAppPurchaseApi { /// Wraps BillingClient#endConnection(BillingClientStateListener). void endConnection(); + /// Wraps BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener). + @async + PlatformBillingResult consumeAsync(String purchaseToken); + /// Wraps BillingClient#isAlternativeBillingOnlyAvailableAsync(). @async PlatformBillingResult isAlternativeBillingOnlyAvailable(); 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 819bfa69f90..acc94b9a71f 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 @@ -25,6 +25,8 @@ void main() { setUp(() { WidgetsFlutterBinding.ensureInitialized(); mockApi = MockInAppPurchaseApi(); + when(mockApi.startConnection(any, any)).thenAnswer( + (_) async => PlatformBillingResult(responseCode: 0, debugMessage: '')); manager = BillingClientManager( billingClientFactory: (PurchasesUpdatedListener listener) => BillingClient(listener, api: mockApi)); @@ -39,7 +41,7 @@ void main() { final Completer connectedCompleter = Completer(); when(mockApi.startConnection(any, any)).thenAnswer((_) async { connectedCompleter.complete(); - return {}; + return PlatformBillingResult(responseCode: 0, debugMessage: ''); }); final Completer calledCompleter1 = Completer(); diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index 79c1e3a4839..b9a03991f62 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -43,6 +43,8 @@ void main() { setUp(() { mockApi = MockInAppPurchaseApi(); + when(mockApi.startConnection(any, any)).thenAnswer( + (_) async => PlatformBillingResult(responseCode: 0, debugMessage: '')); billingClient = BillingClient((PurchasesResultWrapper _) {}, api: mockApi); stubPlatform.reset(); }); @@ -83,10 +85,10 @@ void main() { const String debugMessage = 'dummy message'; const BillingResponse responseCode = BillingResponse.developerError; when(mockApi.startConnection(any, any)).thenAnswer( - (_) async => { - 'responseCode': const BillingResponseConverter().toJson(responseCode), - 'debugMessage': debugMessage, - }, + (_) async => PlatformBillingResult( + responseCode: const BillingResponseConverter().toJson(responseCode), + debugMessage: debugMessage, + ), ); const BillingResultWrapper billingResult = BillingResultWrapper( @@ -537,36 +539,20 @@ void main() { }); group('consume purchases', () { - const String consumeMethodName = - 'BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener)'; test('consume purchase async success', () async { + const String token = 'dummy token'; const BillingResponse expectedCode = BillingResponse.ok; const String debugMessage = 'dummy message'; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: consumeMethodName, - value: buildBillingResultMap(expectedBillingResult)); + when(mockApi.consumeAsync(token)).thenAnswer( + (_) async => convertToPigeonResult(expectedBillingResult)); final BillingResultWrapper billingResult = - await billingClient.consumeAsync('dummy token'); + await billingClient.consumeAsync(token); expect(billingResult, equals(expectedBillingResult)); }); - - test('handles method channel returning null', () async { - stubPlatform.addResponse( - name: consumeMethodName, - ); - final BillingResultWrapper billingResult = - await billingClient.consumeAsync('dummy token'); - - expect( - billingResult, - equals(const BillingResultWrapper( - responseCode: BillingResponse.error, - debugMessage: kInvalidBillingResultErrorMessage))); - }); }); group('acknowledge purchases', () { diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart index ced1ccb3440..9e3ef8f67f2 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart @@ -21,6 +21,17 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +class _FakePlatformBillingResult_0 extends _i1.SmartFake + implements _i2.PlatformBillingResult { + _FakePlatformBillingResult_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [InAppPurchaseApi]. /// /// See the documentation for Mockito's code generation for more information. @@ -36,7 +47,7 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { ) as _i3.Future); @override - _i3.Future> startConnection( + _i3.Future<_i2.PlatformBillingResult> startConnection( int? callbackHandle, _i2.PlatformBillingChoiceMode? billingMode, ) => @@ -48,9 +59,113 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { billingMode, ], ), - returnValue: - _i3.Future>.value({}), - returnValueForMissingStub: - _i3.Future>.value({}), - ) as _i3.Future>); + returnValue: _i3.Future<_i2.PlatformBillingResult>.value( + _FakePlatformBillingResult_0( + this, + Invocation.method( + #startConnection, + [ + callbackHandle, + billingMode, + ], + ), + )), + returnValueForMissingStub: _i3.Future<_i2.PlatformBillingResult>.value( + _FakePlatformBillingResult_0( + this, + Invocation.method( + #startConnection, + [ + callbackHandle, + billingMode, + ], + ), + )), + ) as _i3.Future<_i2.PlatformBillingResult>); + + @override + _i3.Future endConnection() => (super.noSuchMethod( + Invocation.method( + #endConnection, + [], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future<_i2.PlatformBillingResult> consumeAsync(String? purchaseToken) => + (super.noSuchMethod( + Invocation.method( + #consumeAsync, + [purchaseToken], + ), + returnValue: _i3.Future<_i2.PlatformBillingResult>.value( + _FakePlatformBillingResult_0( + this, + Invocation.method( + #consumeAsync, + [purchaseToken], + ), + )), + returnValueForMissingStub: _i3.Future<_i2.PlatformBillingResult>.value( + _FakePlatformBillingResult_0( + this, + Invocation.method( + #consumeAsync, + [purchaseToken], + ), + )), + ) as _i3.Future<_i2.PlatformBillingResult>); + + @override + _i3.Future<_i2.PlatformBillingResult> isAlternativeBillingOnlyAvailable() => + (super.noSuchMethod( + Invocation.method( + #isAlternativeBillingOnlyAvailable, + [], + ), + returnValue: _i3.Future<_i2.PlatformBillingResult>.value( + _FakePlatformBillingResult_0( + this, + Invocation.method( + #isAlternativeBillingOnlyAvailable, + [], + ), + )), + returnValueForMissingStub: _i3.Future<_i2.PlatformBillingResult>.value( + _FakePlatformBillingResult_0( + this, + Invocation.method( + #isAlternativeBillingOnlyAvailable, + [], + ), + )), + ) as _i3.Future<_i2.PlatformBillingResult>); + + @override + _i3.Future<_i2.PlatformBillingResult> + showAlternativeBillingOnlyInformationDialog() => (super.noSuchMethod( + Invocation.method( + #showAlternativeBillingOnlyInformationDialog, + [], + ), + returnValue: _i3.Future<_i2.PlatformBillingResult>.value( + _FakePlatformBillingResult_0( + this, + Invocation.method( + #showAlternativeBillingOnlyInformationDialog, + [], + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.PlatformBillingResult>.value( + _FakePlatformBillingResult_0( + this, + Invocation.method( + #showAlternativeBillingOnlyInformationDialog, + [], + ), + )), + ) as _i3.Future<_i2.PlatformBillingResult>); } diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart index 14cd446bf8a..0b718034863 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart @@ -4,6 +4,7 @@ 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/messages.g.dart'; import 'package:test/test.dart'; const PurchaseWrapper dummyPurchase = PurchaseWrapper( @@ -252,3 +253,16 @@ Map buildBillingResultMap(BillingResultWrapper original) { 'debugMessage': original.debugMessage, }; } + +/// Creates the [PlatformBillingResult] to return from a mock to get +/// [targetResult]. +/// +/// Since [PlatformBillingResult] returns a non-nullable debug string, the +/// target must have a non-null string as well. +PlatformBillingResult convertToPigeonResult(BillingResultWrapper targetResult) { + return PlatformBillingResult( + responseCode: + const BillingResponseConverter().toJson(targetResult.responseCode), + debugMessage: targetResult.debugMessage!, + ); +} 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 9b762484a47..45ec4abfc65 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 @@ -35,6 +35,8 @@ void main() { setUp(() { widgets.WidgetsFlutterBinding.ensureInitialized(); mockApi = MockInAppPurchaseApi(); + when(mockApi.startConnection(any, any)).thenAnswer( + (_) async => PlatformBillingResult(responseCode: 0, debugMessage: '')); manager = BillingClientManager( billingClientFactory: (PurchasesUpdatedListener listener) => BillingClient(listener, api: mockApi)); @@ -42,17 +44,13 @@ void main() { }); group('consume purchases', () { - const String consumeMethodName = - 'BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener)'; test('consume purchase async success', () async { const BillingResponse expectedCode = BillingResponse.ok; const String debugMessage = 'dummy message'; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: consumeMethodName, - value: buildBillingResultMap(expectedBillingResult), - ); + when(mockApi.consumeAsync(any)).thenAnswer( + (_) async => convertToPigeonResult(expectedBillingResult)); final BillingResultWrapper billingResultWrapper = await iapAndroidPlatformAddition.consumePurchase( GooglePlayPurchaseDetails.fromPurchase(dummyPurchase).first); @@ -139,9 +137,10 @@ void main() { const BillingResultWrapper expected = BillingResultWrapper( responseCode: BillingResponse.ok, debugMessage: 'dummy message'); - when(mockApi.showAlternativeBillingOnlyInformationDialog()).thenAnswer( - (_) async => PlatformBillingResult( - responseCode: 0, debugMessage: expected.debugMessage!)); + when(mockApi.isAlternativeBillingOnlyAvailable()) + .thenAnswer((_) async => convertToPigeonResult(expected)); + when(mockApi.showAlternativeBillingOnlyInformationDialog()) + .thenAnswer((_) async => convertToPigeonResult(expected)); final BillingResultWrapper result = await iapAndroidPlatformAddition.isAlternativeBillingOnlyAvailable(); 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 86375cf5a3a..b93b1a720e1 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 @@ -10,6 +10,7 @@ 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/channel.dart'; +import 'package:in_app_purchase_android/src/messages.g.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import 'package:mockito/mockito.dart'; @@ -38,6 +39,8 @@ void main() { widgets.WidgetsFlutterBinding.ensureInitialized(); mockApi = MockInAppPurchaseApi(); + when(mockApi.startConnection(any, any)).thenAnswer( + (_) async => PlatformBillingResult(responseCode: 0, debugMessage: '')); iapAndroidPlatform = InAppPurchaseAndroidPlatform( manager: BillingClientManager( billingClientFactory: (PurchasesUpdatedListener listener) => @@ -49,14 +52,6 @@ void main() { stubPlatform.reset(); }); - test('register sets an instance', () { - InAppPurchaseAndroidPlatform.registerPlatform(); - expect(InAppPurchasePlatform.instance, isA()); - // TODO(stuartmorgan): Refactor tests so that the instance isn't set by - // global test setup, so that this isn't necessary. - expect(InAppPurchasePlatform.instance, isNot(iapAndroidPlatform)); - }); - group('connection management', () { test('connects on initialization', () { //await iapAndroidPlatform.isAvailable(); @@ -86,7 +81,7 @@ void main() { ); when(mockApi.startConnection(any, any)).thenAnswer((_) async { stubPlatform.addResponse(name: acknowledgePurchaseCall, value: okValue); - return okValue; + return PlatformBillingResult(responseCode: 0, debugMessage: ''); }); final PurchaseDetails purchase = GooglePlayPurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase) @@ -330,8 +325,6 @@ void main() { group('make payment', () { const String launchMethodName = 'BillingClient#launchBillingFlow(Activity, BillingFlowParams)'; - const String consumeMethodName = - 'BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener)'; test('buy non consumable, serializes and deserializes data', () async { const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; @@ -478,14 +471,12 @@ void main() { const BillingResultWrapper expectedBillingResultForConsume = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: consumeMethodName, - value: buildBillingResultMap(expectedBillingResultForConsume), - additionalStepBeforeReturn: (dynamic args) { - final String purchaseToken = - (args as Map)['purchaseToken']! as String; - consumeCompleter.complete(purchaseToken); - }); + when(mockApi.consumeAsync(any)).thenAnswer((Invocation invocation) async { + final String purchaseToken = + invocation.positionalArguments.first as String; + consumeCompleter.complete(purchaseToken); + return convertToPigeonResult(expectedBillingResultForConsume); + }); final Completer completer = Completer(); PurchaseDetails purchaseDetails; @@ -596,14 +587,12 @@ void main() { const BillingResultWrapper expectedBillingResultForConsume = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: consumeMethodName, - value: buildBillingResultMap(expectedBillingResultForConsume), - additionalStepBeforeReturn: (dynamic args) { - final String purchaseToken = - (args as Map)['purchaseToken']! as String; - consumeCompleter.complete(purchaseToken); - }); + when(mockApi.consumeAsync(any)).thenAnswer((Invocation invocation) async { + final String purchaseToken = + invocation.positionalArguments.first as String; + consumeCompleter.complete(purchaseToken); + return convertToPigeonResult(expectedBillingResultForConsume); + }); final Completer completer = Completer(); PurchaseDetails purchaseDetails; @@ -675,14 +664,12 @@ void main() { const BillingResultWrapper expectedBillingResultForConsume = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: consumeMethodName, - value: buildBillingResultMap(expectedBillingResultForConsume), - additionalStepBeforeReturn: (dynamic args) { - final String purchaseToken = - (args as Map)['purchaseToken']! as String; - consumeCompleter.complete(purchaseToken); - }); + when(mockApi.consumeAsync(any)).thenAnswer((Invocation invocation) async { + final String purchaseToken = + invocation.positionalArguments.first as String; + consumeCompleter.complete(purchaseToken); + return convertToPigeonResult(expectedBillingResultForConsume); + }); final Stream> purchaseStream = iapAndroidPlatform.purchaseStream; @@ -742,14 +729,12 @@ void main() { const BillingResultWrapper expectedBillingResultForConsume = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: consumeMethodName, - value: buildBillingResultMap(expectedBillingResultForConsume), - additionalStepBeforeReturn: (dynamic args) { - final String purchaseToken = - (args as Map)['purchaseToken']! as String; - consumeCompleter.complete(purchaseToken); - }); + when(mockApi.consumeAsync(any)).thenAnswer((Invocation invocation) async { + final String purchaseToken = + invocation.positionalArguments.first as String; + consumeCompleter.complete(purchaseToken); + return convertToPigeonResult(expectedBillingResultForConsume); + }); final Completer completer = Completer(); PurchaseDetails purchaseDetails; From 2f161c27eb233fe0ca6151699a41ce21ef4325f8 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 5 Mar 2024 12:15:38 -0500 Subject: [PATCH 10/25] Convert isFeatureSupported --- .../plugins/inapppurchase/Messages.java | 28 +++++++++++++++++ .../inapppurchase/MethodCallHandlerImpl.java | 24 +++++++++------ .../inapppurchase/MethodCallHandlerTest.java | 17 +++-------- .../billing_client_wrapper.dart | 7 ++--- .../lib/src/messages.g.dart | 30 +++++++++++++++++++ .../pigeons/messages.dart | 6 ++++ .../billing_client_wrapper_test.dart | 23 ++++---------- .../billing_client_wrapper_test.mocks.dart | 10 +++++++ .../purchase_wrapper_test.dart | 14 --------- ...rchase_android_platform_addition_test.dart | 23 ++++---------- ...in_app_purchase_android_platform_test.dart | 1 + 11 files changed, 106 insertions(+), 77 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java index 22963d1b40c..c4dc029fb93 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -228,6 +228,9 @@ void startConnection( void endConnection(); /** Wraps BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener). */ void consumeAsync(@NonNull String purchaseToken, @NonNull Result result); + /** Wraps BillingClient#isFeatureSupported(String). */ + @NonNull + Boolean isFeatureSupported(@NonNull String feature); /** Wraps BillingClient#isAlternativeBillingOnlyAvailableAsync(). */ void isAlternativeBillingOnlyAvailable(@NonNull Result result); /** Wraps BillingClient#showAlternativeBillingOnlyInformationDialog(). */ @@ -354,6 +357,31 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.isFeatureSupported", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String featureArg = (String) args.get(0); + try { + Boolean output = api.isFeatureSupported(featureArg); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } { BasicMessageChannel channel = new BasicMessageChannel<>( diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index e8437008537..43dcdbf9841 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -62,7 +62,6 @@ static final class MethodNames { "BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener)"; static final String ACKNOWLEDGE_PURCHASE = "BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)"; - static final String IS_FEATURE_SUPPORTED = "BillingClient#isFeatureSupported(String)"; static final String GET_BILLING_CONFIG = "BillingClient#getBillingConfig()"; static final String CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS = "BillingClient#createAlternativeBillingOnlyReportingDetails()"; @@ -171,9 +170,6 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result case MethodNames.ACKNOWLEDGE_PURCHASE: acknowledgePurchase((String) call.argument("purchaseToken"), result); break; - case MethodNames.IS_FEATURE_SUPPORTED: - isFeatureSupported((String) call.argument("feature"), result); - break; case MethodNames.GET_BILLING_CONFIG: getBillingConfig(result); break; @@ -189,6 +185,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result public void showAlternativeBillingOnlyInformationDialog( @NonNull Messages.Result result) { validateBillingClient(); + assert billingClient != null; if (activity == null) { throw new FlutterError(ACTIVITY_UNAVAILABLE, "Not attempting to show dialog", null); } @@ -201,6 +198,7 @@ private void createAlternativeBillingOnlyReportingDetails(final MethodChannel.Re if (billingClientError(result)) { return; } + assert billingClient != null; billingClient.createAlternativeBillingOnlyReportingDetailsAsync( ((billingResult, alternativeBillingOnlyReportingDetails) -> result.success( @@ -212,6 +210,7 @@ private void createAlternativeBillingOnlyReportingDetails(final MethodChannel.Re public void isAlternativeBillingOnlyAvailable( @NonNull Messages.Result result) { validateBillingClient(); + assert billingClient != null; billingClient.isAlternativeBillingOnlyAvailableAsync( billingResult -> result.success(pigeonBillingResultFromBillingResult(billingResult))); } @@ -220,6 +219,7 @@ private void getBillingConfig(final MethodChannel.Result result) { if (billingClientError(result)) { return; } + assert billingClient != null; billingClient.getBillingConfigAsync( GetBillingConfigParams.newBuilder().build(), (billingResult, billingConfig) -> @@ -242,6 +242,7 @@ private void endBillingClientConnection() { @NonNull public Boolean isReady() { validateBillingClient(); + assert billingClient != null; return billingClient.isReady(); } @@ -250,6 +251,7 @@ private void queryProductDetailsAsync( if (billingClientError(result)) { return; } + assert billingClient != null; QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder().setProductList(productList).build(); @@ -277,6 +279,7 @@ private void launchBillingFlow( if (billingClientError(result)) { return; } + assert billingClient != null; com.android.billingclient.api.ProductDetails productDetails = cachedProducts.get(product); if (productDetails == null) { @@ -388,6 +391,7 @@ public void consumeAsync( @NonNull String purchaseToken, @NonNull Messages.Result result) { validateBillingClient(); + assert billingClient != null; ConsumeResponseListener listener = (billingResult, outToken) -> @@ -403,6 +407,7 @@ private void queryPurchasesAsync(String productType, MethodChannel.Result result if (billingClientError(result)) { return; } + assert billingClient != null; // Like in our connect call, consider the billing client responding a "success" here regardless // of status code. @@ -425,6 +430,7 @@ private void queryPurchaseHistoryAsync(String productType, final MethodChannel.R if (billingClientError(result)) { return; } + assert billingClient != null; billingClient.queryPurchaseHistoryAsync( QueryPurchaseHistoryParams.newBuilder().setProductType(productType).build(), @@ -475,6 +481,7 @@ private void acknowledgePurchase(String purchaseToken, final MethodChannel.Resul if (billingClientError(result)) { return; } + assert billingClient != null; AcknowledgePurchaseParams params = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchaseToken).build(); billingClient.acknowledgePurchase( @@ -506,12 +513,11 @@ private void validateBillingClient() { } } - private void isFeatureSupported(String feature, MethodChannel.Result result) { - if (billingClientError(result)) { - return; - } + @Override + public @NonNull Boolean isFeatureSupported(@NonNull String feature) { + validateBillingClient(); assert billingClient != null; BillingResult billingResult = billingClient.isFeatureSupported(feature); - result.success(billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK); + return billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK; } } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index 6b11312f408..c17804a1342 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -8,7 +8,6 @@ import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.ACKNOWLEDGE_PURCHASE; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.GET_BILLING_CONFIG; -import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.IS_FEATURE_SUPPORTED; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.LAUNCH_BILLING_FLOW; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.ON_DISCONNECT; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PRODUCT_DETAILS; @@ -1001,38 +1000,30 @@ public void endConnection_if_activity_detached() { public void isFutureSupported_true() { mockStartConnection(); final String feature = "subscriptions"; - Map arguments = new HashMap<>(); - arguments.put("feature", feature); BillingResult billingResult = BillingResult.newBuilder() .setResponseCode(BillingClient.BillingResponseCode.OK) .setDebugMessage("dummy debug message") .build(); - - MethodCall call = new MethodCall(IS_FEATURE_SUPPORTED, arguments); when(mockBillingClient.isFeatureSupported(feature)).thenReturn(billingResult); - methodChannelHandler.onMethodCall(call, result); - verify(result).success(true); + + assertTrue(methodChannelHandler.isFeatureSupported(feature)); } @Test public void isFutureSupported_false() { mockStartConnection(); final String feature = "subscriptions"; - Map arguments = new HashMap<>(); - arguments.put("feature", feature); BillingResult billingResult = BillingResult.newBuilder() .setResponseCode(BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED) .setDebugMessage("dummy debug message") .build(); - - MethodCall call = new MethodCall(IS_FEATURE_SUPPORTED, arguments); when(mockBillingClient.isFeatureSupported(feature)).thenReturn(billingResult); - methodChannelHandler.onMethodCall(call, result); - verify(result).success(false); + + assertFalse(methodChannelHandler.isFeatureSupported(feature)); } /** 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 6551fa63069..e5b7b0e110e 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 @@ -315,11 +315,8 @@ class BillingClient { /// 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 { - final bool? result = await channel.invokeMethod( - 'BillingClient#isFeatureSupported(String)', { - 'feature': const BillingClientFeatureConverter().toJson(feature), - }); - return result ?? false; + return _hostApi.isFeatureSupported( + const BillingClientFeatureConverter().toJson(feature)); } /// BillingConfig method channel string identifier. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart index 14b17933fbe..4cb28340d21 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -206,6 +206,36 @@ class InAppPurchaseApi { } } + /// Wraps BillingClient#isFeatureSupported(String). + Future isFeatureSupported(String feature) async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.isFeatureSupported'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send([feature]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as bool?)!; + } + } + /// Wraps BillingClient#isAlternativeBillingOnlyAvailableAsync(). Future isAlternativeBillingOnlyAvailable() async { const String __pigeon_channelName = diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart index 414dc5a1339..67330246533 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -48,6 +48,12 @@ abstract class InAppPurchaseApi { @async PlatformBillingResult consumeAsync(String purchaseToken); + /// Wraps BillingClient#isFeatureSupported(String). + // TODO(stuartmorgan): Consider making this take a enum, and converting the + // enum value to string constants on the native side, so that magic strings + // from the Play Billing API aren't duplicated in Dart code. + bool isFeatureSupported(String feature); + /// Wraps BillingClient#isAlternativeBillingOnlyAvailableAsync(). @async PlatformBillingResult isAlternativeBillingOnlyAvailable(); diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index b9a03991f62..f728eb33414 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -11,6 +11,7 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import '../stub_in_app_purchase_platform.dart'; +import '../test_conversion_utils.dart'; import 'billing_client_wrapper_test.mocks.dart'; import 'product_details_wrapper_test.dart'; import 'purchase_wrapper_test.dart'; @@ -589,34 +590,20 @@ void main() { }); group('isFeatureSupported', () { - const String isFeatureSupportedMethodName = - 'BillingClient#isFeatureSupported(String)'; test('isFeatureSupported returns false', () async { - late Map arguments; - stubPlatform.addResponse( - name: isFeatureSupportedMethodName, - value: false, - additionalStepBeforeReturn: (dynamic value) => - arguments = value as Map, - ); + when(mockApi.isFeatureSupported('subscriptions')) + .thenAnswer((_) async => false); final bool isSupported = await billingClient .isFeatureSupported(BillingClientFeature.subscriptions); expect(isSupported, isFalse); - expect(arguments['feature'], equals('subscriptions')); }); test('isFeatureSupported returns true', () async { - late Map arguments; - stubPlatform.addResponse( - name: isFeatureSupportedMethodName, - value: true, - additionalStepBeforeReturn: (dynamic value) => - arguments = value as Map, - ); + when(mockApi.isFeatureSupported('subscriptions')) + .thenAnswer((_) async => true); final bool isSupported = await billingClient .isFeatureSupported(BillingClientFeature.subscriptions); expect(isSupported, isTrue); - expect(arguments['feature'], equals('subscriptions')); }); }); diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart index 9e3ef8f67f2..c33da3408a7 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart @@ -118,6 +118,16 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { )), ) as _i3.Future<_i2.PlatformBillingResult>); + @override + _i3.Future isFeatureSupported(String? feature) => (super.noSuchMethod( + Invocation.method( + #isFeatureSupported, + [feature], + ), + returnValue: _i3.Future.value(false), + returnValueForMissingStub: _i3.Future.value(false), + ) as _i3.Future); + @override _i3.Future<_i2.PlatformBillingResult> isAlternativeBillingOnlyAvailable() => (super.noSuchMethod( diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart index 0b718034863..14cd446bf8a 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart @@ -4,7 +4,6 @@ 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/messages.g.dart'; import 'package:test/test.dart'; const PurchaseWrapper dummyPurchase = PurchaseWrapper( @@ -253,16 +252,3 @@ Map buildBillingResultMap(BillingResultWrapper original) { 'debugMessage': original.debugMessage, }; } - -/// Creates the [PlatformBillingResult] to return from a mock to get -/// [targetResult]. -/// -/// Since [PlatformBillingResult] returns a non-nullable debug string, the -/// target must have a non-null string as well. -PlatformBillingResult convertToPigeonResult(BillingResultWrapper targetResult) { - return PlatformBillingResult( - responseCode: - const BillingResponseConverter().toJson(targetResult.responseCode), - debugMessage: targetResult.debugMessage!, - ); -} 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 45ec4abfc65..1d2458e87ac 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 @@ -16,6 +16,7 @@ import 'billing_client_wrappers/billing_client_wrapper_test.dart'; import 'billing_client_wrappers/billing_client_wrapper_test.mocks.dart'; import 'billing_client_wrappers/purchase_wrapper_test.dart'; import 'stub_in_app_purchase_platform.dart'; +import 'test_conversion_utils.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -230,34 +231,20 @@ void main() { }); group('isFeatureSupported', () { - const String isFeatureSupportedMethodName = - 'BillingClient#isFeatureSupported(String)'; test('isFeatureSupported returns false', () async { - late Map arguments; - stubPlatform.addResponse( - name: isFeatureSupportedMethodName, - value: false, - additionalStepBeforeReturn: (dynamic value) => - arguments = value as Map, - ); + when(mockApi.isFeatureSupported('subscriptions')) + .thenAnswer((_) async => false); final bool isSupported = await iapAndroidPlatformAddition .isFeatureSupported(BillingClientFeature.subscriptions); expect(isSupported, isFalse); - expect(arguments['feature'], equals('subscriptions')); }); test('isFeatureSupported returns true', () async { - late Map arguments; - stubPlatform.addResponse( - name: isFeatureSupportedMethodName, - value: true, - additionalStepBeforeReturn: (dynamic value) => - arguments = value as Map, - ); + when(mockApi.isFeatureSupported('subscriptions')) + .thenAnswer((_) async => true); final bool isSupported = await iapAndroidPlatformAddition .isFeatureSupported(BillingClientFeature.subscriptions); expect(isSupported, isTrue); - expect(arguments['feature'], equals('subscriptions')); }); }); } diff --git a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart index b93b1a720e1..08563c3abd6 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 @@ -18,6 +18,7 @@ import 'billing_client_wrappers/billing_client_wrapper_test.mocks.dart'; import 'billing_client_wrappers/product_details_wrapper_test.dart'; import 'billing_client_wrappers/purchase_wrapper_test.dart'; import 'stub_in_app_purchase_platform.dart'; +import 'test_conversion_utils.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); From b9dda048a059b106440a78abeebb2959f4937f4d Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 5 Mar 2024 12:27:24 -0500 Subject: [PATCH 11/25] Convert acknowledgePurchase --- .../plugins/inapppurchase/Messages.java | 37 +++++++++++++++++++ .../inapppurchase/MethodCallHandlerImpl.java | 17 ++++----- .../inapppurchase/MethodCallHandlerTest.java | 17 ++++----- .../billing_client_wrapper.dart | 9 +---- .../lib/src/messages.g.dart | 31 ++++++++++++++++ .../pigeons/messages.dart | 4 ++ .../billing_client_wrapper_test.dart | 24 ++---------- .../billing_client_wrapper_test.mocks.dart | 26 +++++++++++++ ...in_app_purchase_android_platform_test.dart | 37 ++++++++----------- .../test/test_conversion_utils.dart | 19 ++++++++++ 10 files changed, 153 insertions(+), 68 deletions(-) create mode 100644 packages/in_app_purchase/in_app_purchase_android/test/test_conversion_utils.dart diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java index c4dc029fb93..4a6d0727f7a 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -226,6 +226,12 @@ void startConnection( @NonNull Result result); /** Wraps BillingClient#endConnection(BillingClientStateListener). */ void endConnection(); + /** + * Wraps BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, + * AcknowledgePurchaseResponseListener). + */ + void acknowledgePurchase( + @NonNull String purchaseToken, @NonNull Result result); /** Wraps BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener). */ void consumeAsync(@NonNull String purchaseToken, @NonNull Result result); /** Wraps BillingClient#isFeatureSupported(String). */ @@ -326,6 +332,37 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.acknowledgePurchase", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String purchaseTokenArg = (String) args.get(0); + Result resultCallback = + new Result() { + public void success(PlatformBillingResult result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.acknowledgePurchase(purchaseTokenArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } { BasicMessageChannel channel = new BasicMessageChannel<>( diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index 43dcdbf9841..0ee4b2eb189 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -60,8 +60,6 @@ static final class MethodNames { "BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)"; static final String QUERY_PURCHASE_HISTORY_ASYNC = "BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener)"; - static final String ACKNOWLEDGE_PURCHASE = - "BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)"; static final String GET_BILLING_CONFIG = "BillingClient#getBillingConfig()"; static final String CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS = "BillingClient#createAlternativeBillingOnlyReportingDetails()"; @@ -167,9 +165,6 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result case MethodNames.QUERY_PURCHASE_HISTORY_ASYNC: queryPurchaseHistoryAsync((String) call.argument("productType"), result); break; - case MethodNames.ACKNOWLEDGE_PURCHASE: - acknowledgePurchase((String) call.argument("purchaseToken"), result); - break; case MethodNames.GET_BILLING_CONFIG: getBillingConfig(result); break; @@ -477,15 +472,17 @@ public void onBillingServiceDisconnected() { }); } - private void acknowledgePurchase(String purchaseToken, final MethodChannel.Result result) { - if (billingClientError(result)) { - return; - } + @Override + public void acknowledgePurchase( + @NonNull String purchaseToken, + @NonNull Messages.Result result) { + validateBillingClient(); assert billingClient != null; AcknowledgePurchaseParams params = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchaseToken).build(); billingClient.acknowledgePurchase( - params, billingResult -> result.success(fromBillingResult(billingResult))); + params, + billingResult -> result.success(pigeonBillingResultFromBillingResult(billingResult))); } protected void updateCachedProducts(@Nullable List productDetailsList) { diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index c17804a1342..ca456d1ff45 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -5,7 +5,6 @@ package io.flutter.plugins.inapppurchase; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.ACTIVITY_UNAVAILABLE; -import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.ACKNOWLEDGE_PURCHASE; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.GET_BILLING_CONFIG; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.LAUNCH_BILLING_FLOW; @@ -965,26 +964,26 @@ public void acknowledgePurchase() { .setResponseCode(100) .setDebugMessage("dummy debug message") .build(); - HashMap arguments = new HashMap<>(); - arguments.put("purchaseToken", "mockToken"); - arguments.put("developerPayload", "mockPayload"); + final String purchaseToken = "mockToken"; ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(AcknowledgePurchaseResponseListener.class); - methodChannelHandler.onMethodCall(new MethodCall(ACKNOWLEDGE_PURCHASE, arguments), result); + methodChannelHandler.acknowledgePurchase(purchaseToken, platformBillingResult); AcknowledgePurchaseParams params = - AcknowledgePurchaseParams.newBuilder().setPurchaseToken("mockToken").build(); + AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchaseToken).build(); // Verify we pass the data to result verify(mockBillingClient).acknowledgePurchase(refEq(params), listenerCaptor.capture()); listenerCaptor.getValue().onAcknowledgePurchaseResponse(billingResult); - verify(result).success(resultCaptor.capture()); // Verify we pass the response code to result - verify(result, never()).error(any(), any(), any()); - verify(result, times(1)).success(fromBillingResult(billingResult)); + verify(platformBillingResult, never()).error(any()); + ArgumentCaptor resultCaptor = + ArgumentCaptor.forClass(PlatformBillingResult.class); + verify(platformBillingResult, times(1)).success(resultCaptor.capture()); + assertResultsMatch(resultCaptor.getValue(), billingResult); } @Test 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 e5b7b0e110e..14de0d0a59c 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 @@ -303,13 +303,8 @@ class BillingClient { /// This wraps /// [`BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#acknowledgePurchase(com.android.billingclient.api.AcknowledgePurchaseParams,%20com.android.billingclient.api.AcknowledgePurchaseResponseListener)) Future acknowledgePurchase(String purchaseToken) async { - return BillingResultWrapper.fromJson((await channel.invokeMapMethod( - 'BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)', - { - 'purchaseToken': purchaseToken, - })) ?? - {}); + return resultWrapperFromPlatform( + await _hostApi.acknowledgePurchase(purchaseToken)); } /// Checks if the specified feature or capability is supported by the Play Store. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart index 4cb28340d21..b4326c180d1 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -176,6 +176,37 @@ class InAppPurchaseApi { } } + /// Wraps BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener). + Future acknowledgePurchase( + String purchaseToken) async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.acknowledgePurchase'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send([purchaseToken]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as PlatformBillingResult?)!; + } + } + /// Wraps BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener). Future consumeAsync(String purchaseToken) async { const String __pigeon_channelName = diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart index 67330246533..9597d2beabd 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -44,6 +44,10 @@ abstract class InAppPurchaseApi { /// Wraps BillingClient#endConnection(BillingClientStateListener). void endConnection(); + /// Wraps BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener). + @async + PlatformBillingResult acknowledgePurchase(String purchaseToken); + /// Wraps BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener). @async PlatformBillingResult consumeAsync(String purchaseToken); diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index f728eb33414..7b27b6012c8 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -557,36 +557,20 @@ void main() { }); group('acknowledge purchases', () { - const String acknowledgeMethodName = - 'BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; test('acknowledge purchase success', () async { + const String token = 'dummy token'; const BillingResponse expectedCode = BillingResponse.ok; const String debugMessage = 'dummy message'; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: acknowledgeMethodName, - value: buildBillingResultMap(expectedBillingResult)); + when(mockApi.acknowledgePurchase(token)).thenAnswer( + (_) async => convertToPigeonResult(expectedBillingResult)); final BillingResultWrapper billingResult = - await billingClient.acknowledgePurchase('dummy token'); + await billingClient.acknowledgePurchase(token); expect(billingResult, equals(expectedBillingResult)); }); - - test('handles method channel returning null', () async { - stubPlatform.addResponse( - name: acknowledgeMethodName, - ); - final BillingResultWrapper billingResult = - await billingClient.acknowledgePurchase('dummy token'); - - expect( - billingResult, - equals(const BillingResultWrapper( - responseCode: BillingResponse.error, - debugMessage: kInvalidBillingResultErrorMessage))); - }); }); group('isFeatureSupported', () { diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart index c33da3408a7..a52081a5c60 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart @@ -93,6 +93,32 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { returnValueForMissingStub: _i3.Future.value(), ) as _i3.Future); + @override + _i3.Future<_i2.PlatformBillingResult> acknowledgePurchase( + String? purchaseToken) => + (super.noSuchMethod( + Invocation.method( + #acknowledgePurchase, + [purchaseToken], + ), + returnValue: _i3.Future<_i2.PlatformBillingResult>.value( + _FakePlatformBillingResult_0( + this, + Invocation.method( + #acknowledgePurchase, + [purchaseToken], + ), + )), + returnValueForMissingStub: _i3.Future<_i2.PlatformBillingResult>.value( + _FakePlatformBillingResult_0( + this, + Invocation.method( + #acknowledgePurchase, + [purchaseToken], + ), + )), + ) as _i3.Future<_i2.PlatformBillingResult>); + @override _i3.Future<_i2.PlatformBillingResult> consumeAsync(String? purchaseToken) => (super.noSuchMethod( 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 08563c3abd6..fb1bb972d27 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 @@ -26,8 +26,6 @@ void main() { final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); late MockInAppPurchaseApi mockApi; late InAppPurchaseAndroidPlatform iapAndroidPlatform; - const String acknowledgePurchaseCall = - 'BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; const String onBillingServiceDisconnectedCallback = 'BillingClientStateListener#onBillingServiceDisconnected()'; @@ -70,18 +68,20 @@ void main() { 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, - ), - ), + when(mockApi.acknowledgePurchase(any)).thenAnswer( + (_) async => PlatformBillingResult( + responseCode: const BillingResponseConverter() + .toJson(BillingResponse.serviceDisconnected), + debugMessage: 'disconnected'), ); when(mockApi.startConnection(any, any)).thenAnswer((_) async { - stubPlatform.addResponse(name: acknowledgePurchaseCall, value: okValue); + // Change the acknowledgePurchase response to success for the next call. + when(mockApi.acknowledgePurchase(any)).thenAnswer( + (_) async => PlatformBillingResult( + responseCode: + const BillingResponseConverter().toJson(BillingResponse.ok), + debugMessage: 'disconnected'), + ); return PlatformBillingResult(responseCode: 0, debugMessage: ''); }); final PurchaseDetails purchase = @@ -89,10 +89,7 @@ void main() { .first; final BillingResultWrapper result = await iapAndroidPlatform.completePurchase(purchase); - expect( - stubPlatform.countPreviousCalls(acknowledgePurchaseCall), - equals(2), - ); + verify(mockApi.acknowledgePurchase(any)).called(2); verify(mockApi.startConnection(any, any)).called(2); expect(result.responseCode, equals(BillingResponse.ok)); }); @@ -810,17 +807,13 @@ void main() { }); group('complete purchase', () { - const String completeMethodName = - 'BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; test('complete purchase success', () async { const BillingResponse expectedCode = BillingResponse.ok; const String debugMessage = 'dummy message'; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: completeMethodName, - value: buildBillingResultMap(expectedBillingResult), - ); + when(mockApi.acknowledgePurchase(any)).thenAnswer( + (_) async => convertToPigeonResult(expectedBillingResult)); final PurchaseDetails purchaseDetails = GooglePlayPurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase) .first; diff --git a/packages/in_app_purchase/in_app_purchase_android/test/test_conversion_utils.dart b/packages/in_app_purchase/in_app_purchase_android/test/test_conversion_utils.dart new file mode 100644 index 00000000000..827ce25d749 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/test_conversion_utils.dart @@ -0,0 +1,19 @@ +// 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_android/billing_client_wrappers.dart'; +import 'package:in_app_purchase_android/src/messages.g.dart'; + +/// Creates the [PlatformBillingResult] to return from a mock to get +/// [targetResult]. +/// +/// Since [PlatformBillingResult] returns a non-nullable debug string, the +/// target must have a non-null string as well. +PlatformBillingResult convertToPigeonResult(BillingResultWrapper targetResult) { + return PlatformBillingResult( + responseCode: + const BillingResponseConverter().toJson(targetResult.responseCode), + debugMessage: targetResult.debugMessage!, + ); +} From a7ddda7bde2372abf8940d8e33848f196c2774c0 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 5 Mar 2024 14:39:53 -0500 Subject: [PATCH 12/25] Convert launchBillingFlow --- .../plugins/inapppurchase/Messages.java | 229 +++++++++++- .../inapppurchase/MethodCallHandlerImpl.java | 98 +++--- .../inapppurchase/MethodCallHandlerTest.java | 221 ++++++------ .../billing_client_wrapper.dart | 26 +- .../lib/src/messages.g.dart | 90 ++++- .../pigeons/messages.dart | 27 ++ .../billing_client_wrapper_test.dart | 137 +++----- .../billing_client_wrapper_test.mocks.dart | 26 ++ ...in_app_purchase_android_platform_test.dart | 329 +++++++++--------- 9 files changed, 747 insertions(+), 436 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java index 4a6d0727f7a..4b5e90609dc 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -163,6 +163,200 @@ ArrayList toList() { } } + /** + * Pigeon version of Java BillingFlowParams. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PlatformBillingFlowParams { + private @NonNull String product; + + public @NonNull String getProduct() { + return product; + } + + public void setProduct(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"product\" is null."); + } + this.product = setterArg; + } + + private @NonNull Long prorationMode; + + public @NonNull Long getProrationMode() { + return prorationMode; + } + + public void setProrationMode(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"prorationMode\" is null."); + } + this.prorationMode = setterArg; + } + + private @Nullable String offerToken; + + public @Nullable String getOfferToken() { + return offerToken; + } + + public void setOfferToken(@Nullable String setterArg) { + this.offerToken = setterArg; + } + + private @Nullable String accountId; + + public @Nullable String getAccountId() { + return accountId; + } + + public void setAccountId(@Nullable String setterArg) { + this.accountId = setterArg; + } + + private @Nullable String obfuscatedProfileId; + + public @Nullable String getObfuscatedProfileId() { + return obfuscatedProfileId; + } + + public void setObfuscatedProfileId(@Nullable String setterArg) { + this.obfuscatedProfileId = setterArg; + } + + private @Nullable String oldProduct; + + public @Nullable String getOldProduct() { + return oldProduct; + } + + public void setOldProduct(@Nullable String setterArg) { + this.oldProduct = setterArg; + } + + private @Nullable String purchaseToken; + + public @Nullable String getPurchaseToken() { + return purchaseToken; + } + + public void setPurchaseToken(@Nullable String setterArg) { + this.purchaseToken = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PlatformBillingFlowParams() {} + + public static final class Builder { + + private @Nullable String product; + + @CanIgnoreReturnValue + public @NonNull Builder setProduct(@NonNull String setterArg) { + this.product = setterArg; + return this; + } + + private @Nullable Long prorationMode; + + @CanIgnoreReturnValue + public @NonNull Builder setProrationMode(@NonNull Long setterArg) { + this.prorationMode = setterArg; + return this; + } + + private @Nullable String offerToken; + + @CanIgnoreReturnValue + public @NonNull Builder setOfferToken(@Nullable String setterArg) { + this.offerToken = setterArg; + return this; + } + + private @Nullable String accountId; + + @CanIgnoreReturnValue + public @NonNull Builder setAccountId(@Nullable String setterArg) { + this.accountId = setterArg; + return this; + } + + private @Nullable String obfuscatedProfileId; + + @CanIgnoreReturnValue + public @NonNull Builder setObfuscatedProfileId(@Nullable String setterArg) { + this.obfuscatedProfileId = setterArg; + return this; + } + + private @Nullable String oldProduct; + + @CanIgnoreReturnValue + public @NonNull Builder setOldProduct(@Nullable String setterArg) { + this.oldProduct = setterArg; + return this; + } + + private @Nullable String purchaseToken; + + @CanIgnoreReturnValue + public @NonNull Builder setPurchaseToken(@Nullable String setterArg) { + this.purchaseToken = setterArg; + return this; + } + + public @NonNull PlatformBillingFlowParams build() { + PlatformBillingFlowParams pigeonReturn = new PlatformBillingFlowParams(); + pigeonReturn.setProduct(product); + pigeonReturn.setProrationMode(prorationMode); + pigeonReturn.setOfferToken(offerToken); + pigeonReturn.setAccountId(accountId); + pigeonReturn.setObfuscatedProfileId(obfuscatedProfileId); + pigeonReturn.setOldProduct(oldProduct); + pigeonReturn.setPurchaseToken(purchaseToken); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(7); + toListResult.add(product); + toListResult.add(prorationMode); + toListResult.add(offerToken); + toListResult.add(accountId); + toListResult.add(obfuscatedProfileId); + toListResult.add(oldProduct); + toListResult.add(purchaseToken); + return toListResult; + } + + static @NonNull PlatformBillingFlowParams fromList(@NonNull ArrayList list) { + PlatformBillingFlowParams pigeonResult = new PlatformBillingFlowParams(); + Object product = list.get(0); + pigeonResult.setProduct((String) product); + Object prorationMode = list.get(1); + pigeonResult.setProrationMode( + (prorationMode == null) + ? null + : ((prorationMode instanceof Integer) + ? (Integer) prorationMode + : (Long) prorationMode)); + Object offerToken = list.get(2); + pigeonResult.setOfferToken((String) offerToken); + Object accountId = list.get(3); + pigeonResult.setAccountId((String) accountId); + Object obfuscatedProfileId = list.get(4); + pigeonResult.setObfuscatedProfileId((String) obfuscatedProfileId); + Object oldProduct = list.get(5); + pigeonResult.setOldProduct((String) oldProduct); + Object purchaseToken = list.get(6); + pigeonResult.setPurchaseToken((String) purchaseToken); + return pigeonResult; + } + } + /** Asynchronous error handling return type for non-nullable API method returns. */ public interface Result { /** Success case callback method for handling returns. */ @@ -197,6 +391,8 @@ private InAppPurchaseApiCodec() {} protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { case (byte) 128: + return PlatformBillingFlowParams.fromList((ArrayList) readValue(buffer)); + case (byte) 129: return PlatformBillingResult.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -205,8 +401,11 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { @Override protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { - if (value instanceof PlatformBillingResult) { + if (value instanceof PlatformBillingFlowParams) { stream.write(128); + writeValue(stream, ((PlatformBillingFlowParams) value).toList()); + } else if (value instanceof PlatformBillingResult) { + stream.write(129); writeValue(stream, ((PlatformBillingResult) value).toList()); } else { super.writeValue(stream, value); @@ -226,6 +425,9 @@ void startConnection( @NonNull Result result); /** Wraps BillingClient#endConnection(BillingClientStateListener). */ void endConnection(); + /** Wraps BillingClient#launchBillingFlow(Activity, BillingFlowParams). */ + @NonNull + PlatformBillingResult launchBillingFlow(@NonNull PlatformBillingFlowParams params); /** * Wraps BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, * AcknowledgePurchaseResponseListener). @@ -332,6 +534,31 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.launchBillingFlow", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + PlatformBillingFlowParams paramsArg = (PlatformBillingFlowParams) args.get(0); + try { + PlatformBillingResult output = api.launchBillingFlow(paramsArg); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } { BasicMessageChannel channel = new BasicMessageChannel<>( diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index 0ee4b2eb189..66048168eb6 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -54,8 +54,6 @@ static final class MethodNames { static final String ON_DISCONNECT = "BillingClientStateListener#onBillingServiceDisconnected()"; static final String QUERY_PRODUCT_DETAILS = "BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener)"; - static final String LAUNCH_BILLING_FLOW = - "BillingClient#launchBillingFlow(Activity, BillingFlowParams)"; static final String QUERY_PURCHASES_ASYNC = "BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)"; static final String QUERY_PURCHASE_HISTORY_ASYNC = @@ -71,7 +69,8 @@ private MethodNames() {} // ReplacementMode enum values. // https://github.com/flutter/flutter/issues/128957. @SuppressWarnings(value = "deprecation") - private static final int PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY = + @VisibleForTesting + static final int PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY = com.android.billingclient.api.BillingFlowParams.ProrationMode .UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY; @@ -146,19 +145,6 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result List productList = toProductList(call.argument("productList")); queryProductDetailsAsync(productList, result); break; - case MethodNames.LAUNCH_BILLING_FLOW: - launchBillingFlow( - (String) call.argument("product"), - (String) call.argument("offerToken"), - (String) call.argument("accountId"), - (String) call.argument("obfuscatedProfileId"), - (String) call.argument("oldProduct"), - (String) call.argument("purchaseToken"), - call.hasArgument("prorationMode") - ? (int) call.argument("prorationMode") - : PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY, - result); - break; case MethodNames.QUERY_PURCHASES_ASYNC: queryPurchasesAsync((String) call.argument("productType"), result); break; @@ -262,30 +248,22 @@ private void queryProductDetailsAsync( }); } - private void launchBillingFlow( - String product, - @Nullable String offerToken, - @Nullable String accountId, - @Nullable String obfuscatedProfileId, - @Nullable String oldProduct, - @Nullable String purchaseToken, - int prorationMode, - MethodChannel.Result result) { - if (billingClientError(result)) { - return; - } + @Override + public @NonNull Messages.PlatformBillingResult launchBillingFlow( + @NonNull Messages.PlatformBillingFlowParams params) { + validateBillingClient(); assert billingClient != null; - com.android.billingclient.api.ProductDetails productDetails = cachedProducts.get(product); + com.android.billingclient.api.ProductDetails productDetails = + cachedProducts.get(params.getProduct()); if (productDetails == null) { - result.error( + throw new FlutterError( "NOT_FOUND", "Details for product " - + product + + params.getProduct() + " are not available. It might because products were not fetched prior to the call. Please fetch the products first. An example of how to fetch the products could be found here: " + LOAD_PRODUCT_DOC_URL, null); - return; } @Nullable @@ -294,58 +272,57 @@ private void launchBillingFlow( if (subscriptionOfferDetails != null) { boolean isValidOfferToken = false; for (ProductDetails.SubscriptionOfferDetails offerDetails : subscriptionOfferDetails) { - if (offerToken != null && offerToken.equals(offerDetails.getOfferToken())) { + if (params.getOfferToken() != null + && params.getOfferToken().equals(offerDetails.getOfferToken())) { isValidOfferToken = true; break; } } if (!isValidOfferToken) { - result.error( + throw new FlutterError( "INVALID_OFFER_TOKEN", "Offer token " - + offerToken + + params.getOfferToken() + " for product " - + product + + params.getProduct() + " is not valid. Make sure to only pass offer tokens that belong to the product. To obtain offer tokens for a product, fetch the products. An example of how to fetch the products could be found here: " + LOAD_PRODUCT_DOC_URL, null); - return; } } - if (oldProduct == null - && prorationMode != PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY) { - result.error( + if (params.getOldProduct() == null + && params.getProrationMode() + != PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY) { + throw new FlutterError( "IN_APP_PURCHASE_REQUIRE_OLD_PRODUCT", "launchBillingFlow failed because oldProduct is null. You must provide a valid oldProduct in order to use a proration mode.", null); - return; - } else if (oldProduct != null && !cachedProducts.containsKey(oldProduct)) { - result.error( + } else if (params.getOldProduct() != null + && !cachedProducts.containsKey(params.getOldProduct())) { + throw new FlutterError( "IN_APP_PURCHASE_INVALID_OLD_PRODUCT", "Details for product " - + oldProduct + + params.getOldProduct() + " are not available. It might because products were not fetched prior to the call. Please fetch the products first. An example of how to fetch the products could be found here: " + LOAD_PRODUCT_DOC_URL, null); - return; } if (activity == null) { - result.error( + throw new FlutterError( ACTIVITY_UNAVAILABLE, "Details for product " - + product + + params.getProduct() + " are not available. This method must be run with the app in foreground.", null); - return; } BillingFlowParams.ProductDetailsParams.Builder productDetailsParamsBuilder = BillingFlowParams.ProductDetailsParams.newBuilder(); productDetailsParamsBuilder.setProductDetails(productDetails); - if (offerToken != null) { - productDetailsParamsBuilder.setOfferToken(offerToken); + if (params.getOfferToken() != null) { + productDetailsParamsBuilder.setOfferToken(params.getOfferToken()); } List productDetailsParamsList = new ArrayList<>(); @@ -353,22 +330,25 @@ private void launchBillingFlow( BillingFlowParams.Builder paramsBuilder = BillingFlowParams.newBuilder().setProductDetailsParamsList(productDetailsParamsList); - if (accountId != null && !accountId.isEmpty()) { - paramsBuilder.setObfuscatedAccountId(accountId); + if (params.getAccountId() != null && !params.getAccountId().isEmpty()) { + paramsBuilder.setObfuscatedAccountId(params.getAccountId()); } - if (obfuscatedProfileId != null && !obfuscatedProfileId.isEmpty()) { - paramsBuilder.setObfuscatedProfileId(obfuscatedProfileId); + if (params.getObfuscatedProfileId() != null && !params.getObfuscatedProfileId().isEmpty()) { + paramsBuilder.setObfuscatedProfileId(params.getObfuscatedProfileId()); } BillingFlowParams.SubscriptionUpdateParams.Builder subscriptionUpdateParamsBuilder = BillingFlowParams.SubscriptionUpdateParams.newBuilder(); - if (oldProduct != null && !oldProduct.isEmpty() && purchaseToken != null) { - subscriptionUpdateParamsBuilder.setOldPurchaseToken(purchaseToken); + if (params.getOldProduct() != null + && !params.getOldProduct().isEmpty() + && params.getPurchaseToken() != null) { + subscriptionUpdateParamsBuilder.setOldPurchaseToken(params.getPurchaseToken()); // Set the prorationMode using a helper to minimize impact of deprecation warning suppression. - setReplaceProrationMode(subscriptionUpdateParamsBuilder, prorationMode); + setReplaceProrationMode( + subscriptionUpdateParamsBuilder, params.getProrationMode().intValue()); paramsBuilder.setSubscriptionUpdateParams(subscriptionUpdateParamsBuilder.build()); } - result.success( - fromBillingResult(billingClient.launchBillingFlow(activity, paramsBuilder.build()))); + return pigeonBillingResultFromBillingResult( + billingClient.launchBillingFlow(activity, paramsBuilder.build())); } // TODO(gmackall): Replace uses of deprecated setReplaceProrationMode. diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index ca456d1ff45..af2cdefc2d3 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -7,11 +7,11 @@ import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.ACTIVITY_UNAVAILABLE; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.GET_BILLING_CONFIG; -import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.LAUNCH_BILLING_FLOW; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.ON_DISCONNECT; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PRODUCT_DETAILS; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PURCHASES_ASYNC; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC; +import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY; import static io.flutter.plugins.inapppurchase.PluginPurchaseListener.ON_PURCHASES_UPDATED; import static io.flutter.plugins.inapppurchase.Translator.fromAlternativeBillingOnlyReportingDetails; import static io.flutter.plugins.inapppurchase.Translator.fromBillingConfig; @@ -72,6 +72,7 @@ import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingChoiceMode; +import io.flutter.plugins.inapppurchase.Messages.PlatformBillingFlowParams; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingResult; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -488,11 +489,10 @@ public void launchBillingFlow_null_AccountId_do_not_crash() { // Fetch the product details first and then prepare the launch billing flow call String productId = "foo"; queryForProducts(singletonList(productId)); - HashMap arguments = new HashMap<>(); - arguments.put("product", productId); - arguments.put("accountId", null); - arguments.put("obfuscatedProfileId", null); - MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); + paramsBuilder.setProduct(productId); + paramsBuilder.setProrationMode( + (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); // Launch the billing flow BillingResult billingResult = @@ -501,16 +501,14 @@ public void launchBillingFlow_null_AccountId_do_not_crash() { .setDebugMessage("dummy debug message") .build(); when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); - methodChannelHandler.onMethodCall(launchCall, result); + PlatformBillingResult platformResult = + methodChannelHandler.launchBillingFlow(paramsBuilder.build()); // Verify we pass the arguments to the billing flow ArgumentCaptor billingFlowParamsCaptor = ArgumentCaptor.forClass(BillingFlowParams.class); verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); - - // Verify we pass the response code to result - verify(result, never()).error(any(), any(), any()); - verify(result, times(1)).success(fromBillingResult(billingResult)); + assertResultsMatch(platformResult, billingResult); } @Test @@ -519,11 +517,11 @@ public void launchBillingFlow_ok_null_OldProduct() { String productId = "foo"; String accountId = "account"; queryForProducts(singletonList(productId)); - HashMap arguments = new HashMap<>(); - arguments.put("product", productId); - arguments.put("accountId", accountId); - arguments.put("oldProduct", null); - MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); + paramsBuilder.setProduct(productId); + paramsBuilder.setAccountId(accountId); + paramsBuilder.setProrationMode( + (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); // Launch the billing flow BillingResult billingResult = @@ -532,16 +530,16 @@ public void launchBillingFlow_ok_null_OldProduct() { .setDebugMessage("dummy debug message") .build(); when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); - methodChannelHandler.onMethodCall(launchCall, result); + PlatformBillingResult platformResult = + methodChannelHandler.launchBillingFlow(paramsBuilder.build()); // Verify we pass the arguments to the billing flow ArgumentCaptor billingFlowParamsCaptor = ArgumentCaptor.forClass(BillingFlowParams.class); verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); - // Verify we pass the response code to result - verify(result, never()).error(any(), any(), any()); - verify(result, times(1)).success(fromBillingResult(billingResult)); + // Verify the response. + assertResultsMatch(platformResult, billingResult); } @Test @@ -552,14 +550,19 @@ public void launchBillingFlow_ok_null_Activity() { String productId = "foo"; String accountId = "account"; queryForProducts(singletonList(productId)); - HashMap arguments = new HashMap<>(); - arguments.put("product", productId); - arguments.put("accountId", accountId); - MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); - methodChannelHandler.onMethodCall(launchCall, result); + PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); + paramsBuilder.setProduct(productId); + paramsBuilder.setAccountId(accountId); + paramsBuilder.setProrationMode( + (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); - // Verify we pass the response code to result - verify(result).error(contains("ACTIVITY_UNAVAILABLE"), contains("foreground"), any()); + // Verify the error response. + Messages.FlutterError exception = + assertThrows( + Messages.FlutterError.class, + () -> methodChannelHandler.launchBillingFlow(paramsBuilder.build())); + assertEquals("ACTIVITY_UNAVAILABLE", exception.code); + assertTrue(Objects.requireNonNull(exception.getMessage()).contains("foreground")); verify(result, never()).success(any()); } @@ -570,11 +573,12 @@ public void launchBillingFlow_ok_oldProduct() { String accountId = "account"; String oldProductId = "oldFoo"; queryForProducts(unmodifiableList(asList(productId, oldProductId))); - HashMap arguments = new HashMap<>(); - arguments.put("product", productId); - arguments.put("accountId", accountId); - arguments.put("oldProduct", oldProductId); - MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); + paramsBuilder.setProduct(productId); + paramsBuilder.setAccountId(accountId); + paramsBuilder.setOldProduct(oldProductId); + paramsBuilder.setProrationMode( + (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); // Launch the billing flow BillingResult billingResult = @@ -583,16 +587,16 @@ public void launchBillingFlow_ok_oldProduct() { .setDebugMessage("dummy debug message") .build(); when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); - methodChannelHandler.onMethodCall(launchCall, result); + PlatformBillingResult platformResult = + methodChannelHandler.launchBillingFlow(paramsBuilder.build()); // Verify we pass the arguments to the billing flow ArgumentCaptor billingFlowParamsCaptor = ArgumentCaptor.forClass(BillingFlowParams.class); verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); - // Verify we pass the response code to result - verify(result, never()).error(any(), any(), any()); - verify(result, times(1)).success(fromBillingResult(billingResult)); + // Verify the response. + assertResultsMatch(platformResult, billingResult); } @Test @@ -601,10 +605,11 @@ public void launchBillingFlow_ok_AccountId() { String productId = "foo"; String accountId = "account"; queryForProducts(singletonList(productId)); - HashMap arguments = new HashMap<>(); - arguments.put("product", productId); - arguments.put("accountId", accountId); - MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); + paramsBuilder.setProduct(productId); + paramsBuilder.setAccountId(accountId); + paramsBuilder.setProrationMode( + (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); // Launch the billing flow BillingResult billingResult = @@ -613,16 +618,16 @@ public void launchBillingFlow_ok_AccountId() { .setDebugMessage("dummy debug message") .build(); when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); - methodChannelHandler.onMethodCall(launchCall, result); + PlatformBillingResult platformResult = + methodChannelHandler.launchBillingFlow(paramsBuilder.build()); // Verify we pass the arguments to the billing flow ArgumentCaptor billingFlowParamsCaptor = ArgumentCaptor.forClass(BillingFlowParams.class); verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); - // Verify we pass the response code to result - verify(result, never()).error(any(), any(), any()); - verify(result, times(1)).success(fromBillingResult(billingResult)); + // Verify the response. + assertResultsMatch(platformResult, billingResult); } // TODO(gmackall): Replace uses of deprecated ProrationMode enum values with new @@ -638,13 +643,12 @@ public void launchBillingFlow_ok_Proration() { String accountId = "account"; int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE; queryForProducts(unmodifiableList(asList(productId, oldProductId))); - HashMap arguments = new HashMap<>(); - arguments.put("product", productId); - arguments.put("accountId", accountId); - arguments.put("oldProduct", oldProductId); - arguments.put("purchaseToken", purchaseToken); - arguments.put("prorationMode", prorationMode); - MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); + paramsBuilder.setProduct(productId); + paramsBuilder.setAccountId(accountId); + paramsBuilder.setOldProduct(oldProductId); + paramsBuilder.setPurchaseToken(purchaseToken); + paramsBuilder.setProrationMode((long) prorationMode); // Launch the billing flow BillingResult billingResult = @@ -653,16 +657,16 @@ public void launchBillingFlow_ok_Proration() { .setDebugMessage("dummy debug message") .build(); when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); - methodChannelHandler.onMethodCall(launchCall, result); + PlatformBillingResult platformResult = + methodChannelHandler.launchBillingFlow(paramsBuilder.build()); // Verify we pass the arguments to the billing flow ArgumentCaptor billingFlowParamsCaptor = ArgumentCaptor.forClass(BillingFlowParams.class); verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); - // Verify we pass the response code to result - verify(result, never()).error(any(), any(), any()); - verify(result, times(1)).success(fromBillingResult(billingResult)); + // Verify the response. + assertResultsMatch(platformResult, billingResult); } // TODO(gmackall): Replace uses of deprecated ProrationMode enum values with new @@ -677,12 +681,11 @@ public void launchBillingFlow_ok_Proration_with_null_OldProduct() { String queryOldProductId = "oldFoo"; int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE; queryForProducts(unmodifiableList(asList(productId, queryOldProductId))); - HashMap arguments = new HashMap<>(); - arguments.put("product", productId); - arguments.put("accountId", accountId); - arguments.put("oldProduct", null); - arguments.put("prorationMode", prorationMode); - MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); + paramsBuilder.setProduct(productId); + paramsBuilder.setAccountId(accountId); + paramsBuilder.setOldProduct(null); + paramsBuilder.setProrationMode((long) prorationMode); // Launch the billing flow BillingResult billingResult = @@ -691,15 +694,16 @@ public void launchBillingFlow_ok_Proration_with_null_OldProduct() { .setDebugMessage("dummy debug message") .build(); when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); - methodChannelHandler.onMethodCall(launchCall, result); // Assert that we sent an error back. - verify(result) - .error( - contains("IN_APP_PURCHASE_REQUIRE_OLD_PRODUCT"), - contains("launchBillingFlow failed because oldProduct is null"), - any()); - verify(result, never()).success(any()); + Messages.FlutterError exception = + assertThrows( + Messages.FlutterError.class, + () -> methodChannelHandler.launchBillingFlow(paramsBuilder.build())); + assertEquals("IN_APP_PURCHASE_REQUIRE_OLD_PRODUCT", exception.code); + assertTrue( + Objects.requireNonNull(exception.getMessage()) + .contains("launchBillingFlow failed because oldProduct is null")); } // TODO(gmackall): Replace uses of deprecated ProrationMode enum values with new @@ -715,13 +719,12 @@ public void launchBillingFlow_ok_Full() { String accountId = "account"; int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE; queryForProducts(unmodifiableList(asList(productId, oldProductId))); - HashMap arguments = new HashMap<>(); - arguments.put("product", productId); - arguments.put("accountId", accountId); - arguments.put("oldProduct", oldProductId); - arguments.put("purchaseToken", purchaseToken); - arguments.put("prorationMode", prorationMode); - MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); + paramsBuilder.setProduct(productId); + paramsBuilder.setAccountId(accountId); + paramsBuilder.setOldProduct(oldProductId); + paramsBuilder.setPurchaseToken(purchaseToken); + paramsBuilder.setProrationMode((long) prorationMode); // Launch the billing flow BillingResult billingResult = @@ -730,16 +733,16 @@ public void launchBillingFlow_ok_Full() { .setDebugMessage("dummy debug message") .build(); when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); - methodChannelHandler.onMethodCall(launchCall, result); + PlatformBillingResult platformResult = + methodChannelHandler.launchBillingFlow(paramsBuilder.build()); // Verify we pass the arguments to the billing flow ArgumentCaptor billingFlowParamsCaptor = ArgumentCaptor.forClass(BillingFlowParams.class); verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); - // Verify we pass the response code to result - verify(result, never()).error(any(), any(), any()); - verify(result, times(1)).success(fromBillingResult(billingResult)); + // Verify the response. + assertResultsMatch(platformResult, billingResult); } @Test @@ -748,16 +751,19 @@ public void launchBillingFlow_clientDisconnected() { methodChannelHandler.endConnection(); String productId = "foo"; String accountId = "account"; - HashMap arguments = new HashMap<>(); - arguments.put("product", productId); - arguments.put("accountId", accountId); - MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); - - methodChannelHandler.onMethodCall(launchCall, result); + PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); + paramsBuilder.setProduct(productId); + paramsBuilder.setAccountId(accountId); + paramsBuilder.setProrationMode( + (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); // Assert that we sent an error back. - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); - verify(result, never()).success(any()); + Messages.FlutterError exception = + assertThrows( + Messages.FlutterError.class, + () -> methodChannelHandler.launchBillingFlow(paramsBuilder.build())); + assertEquals("UNAVAILABLE", exception.code); + assertTrue(Objects.requireNonNull(exception.getMessage()).contains("BillingClient")); } @Test @@ -766,16 +772,19 @@ public void launchBillingFlow_productNotFound() { establishConnectedBillingClient(); String productId = "foo"; String accountId = "account"; - HashMap arguments = new HashMap<>(); - arguments.put("product", productId); - arguments.put("accountId", accountId); - MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); - - methodChannelHandler.onMethodCall(launchCall, result); + PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); + paramsBuilder.setProduct(productId); + paramsBuilder.setAccountId(accountId); + paramsBuilder.setProrationMode( + (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); // Assert that we sent an error back. - verify(result).error(contains("NOT_FOUND"), contains(productId), any()); - verify(result, never()).success(any()); + Messages.FlutterError exception = + assertThrows( + Messages.FlutterError.class, + () -> methodChannelHandler.launchBillingFlow(paramsBuilder.build())); + assertEquals("NOT_FOUND", exception.code); + assertTrue(Objects.requireNonNull(exception.getMessage()).contains(productId)); } @Test @@ -786,18 +795,20 @@ public void launchBillingFlow_oldProductNotFound() { String accountId = "account"; String oldProductId = "oldProduct"; queryForProducts(singletonList(productId)); - HashMap arguments = new HashMap<>(); - arguments.put("product", productId); - arguments.put("accountId", accountId); - arguments.put("oldProduct", oldProductId); - MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); - - methodChannelHandler.onMethodCall(launchCall, result); + PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); + paramsBuilder.setProduct(productId); + paramsBuilder.setAccountId(accountId); + paramsBuilder.setOldProduct(oldProductId); + paramsBuilder.setProrationMode( + (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); // Assert that we sent an error back. - verify(result) - .error(contains("IN_APP_PURCHASE_INVALID_OLD_PRODUCT"), contains(oldProductId), any()); - verify(result, never()).success(any()); + Messages.FlutterError exception = + assertThrows( + Messages.FlutterError.class, + () -> methodChannelHandler.launchBillingFlow(paramsBuilder.build())); + assertEquals("IN_APP_PURCHASE_INVALID_OLD_PRODUCT", exception.code); + assertTrue(Objects.requireNonNull(exception.getMessage()).contains(oldProductId)); } @Test 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 14de0d0a59c..17fd816a370 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 @@ -210,21 +210,17 @@ class BillingClient { ProrationMode? prorationMode}) async { assert((oldProduct == null) == (purchaseToken == null), 'oldProduct and purchaseToken must both be set, or both be null.'); - final Map arguments = { - 'product': product, - 'offerToken': offerToken, - 'accountId': accountId, - 'obfuscatedProfileId': obfuscatedProfileId, - 'oldProduct': oldProduct, - 'purchaseToken': purchaseToken, - 'prorationMode': const ProrationModeConverter().toJson(prorationMode ?? - ProrationMode.unknownSubscriptionUpgradeDowngradePolicy) - }; - return BillingResultWrapper.fromJson( - (await channel.invokeMapMethod( - 'BillingClient#launchBillingFlow(Activity, BillingFlowParams)', - arguments)) ?? - {}); + return resultWrapperFromPlatform( + await _hostApi.launchBillingFlow(PlatformBillingFlowParams( + product: product, + prorationMode: const ProrationModeConverter().toJson(prorationMode ?? + ProrationMode.unknownSubscriptionUpgradeDowngradePolicy), + offerToken: offerToken, + accountId: accountId, + obfuscatedProfileId: obfuscatedProfileId, + oldProduct: oldProduct, + purchaseToken: purchaseToken, + ))); } /// Fetches recent purchases for the given [ProductType]. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart index b4326c180d1..87836f98ce4 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -56,13 +56,68 @@ class PlatformBillingResult { } } +/// Pigeon version of Java BillingFlowParams. +class PlatformBillingFlowParams { + PlatformBillingFlowParams({ + required this.product, + required this.prorationMode, + this.offerToken, + this.accountId, + this.obfuscatedProfileId, + this.oldProduct, + this.purchaseToken, + }); + + String product; + + int prorationMode; + + String? offerToken; + + String? accountId; + + String? obfuscatedProfileId; + + String? oldProduct; + + String? purchaseToken; + + Object encode() { + return [ + product, + prorationMode, + offerToken, + accountId, + obfuscatedProfileId, + oldProduct, + purchaseToken, + ]; + } + + static PlatformBillingFlowParams decode(Object result) { + result as List; + return PlatformBillingFlowParams( + product: result[0]! as String, + prorationMode: result[1]! as int, + offerToken: result[2] as String?, + accountId: result[3] as String?, + obfuscatedProfileId: result[4] as String?, + oldProduct: result[5] as String?, + purchaseToken: result[6] as String?, + ); + } +} + class _InAppPurchaseApiCodec extends StandardMessageCodec { const _InAppPurchaseApiCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { - if (value is PlatformBillingResult) { + if (value is PlatformBillingFlowParams) { buffer.putUint8(128); writeValue(buffer, value.encode()); + } else if (value is PlatformBillingResult) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -72,6 +127,8 @@ class _InAppPurchaseApiCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 128: + return PlatformBillingFlowParams.decode(readValue(buffer)!); + case 129: return PlatformBillingResult.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -176,6 +233,37 @@ class InAppPurchaseApi { } } + /// Wraps BillingClient#launchBillingFlow(Activity, BillingFlowParams). + Future launchBillingFlow( + PlatformBillingFlowParams params) async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.launchBillingFlow'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send([params]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as PlatformBillingResult?)!; + } + } + /// Wraps BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener). Future acknowledgePurchase( String purchaseToken) async { diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart index 9597d2beabd..040c6329932 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -20,6 +20,30 @@ class PlatformBillingResult { final String debugMessage; } +/// Pigeon version of Java BillingFlowParams. +class PlatformBillingFlowParams { + PlatformBillingFlowParams({ + required this.product, + required this.prorationMode, + required this.offerToken, + required this.accountId, + required this.obfuscatedProfileId, + required this.oldProduct, + required this.purchaseToken, + }); + + final String product; + // Ideally this would be replaced with an enum on the dart side that maps + // to constants on the Java side, but it's deprecated anyway so that will be + // resolved during the update to the new API. + final int prorationMode; + final String? offerToken; + final String? accountId; + final String? obfuscatedProfileId; + final String? oldProduct; + final String? purchaseToken; +} + /// Pigeon version of billing_client_wrapper.dart's BillingChoiceMode. enum PlatformBillingChoiceMode { /// Billing through google play. @@ -44,6 +68,9 @@ abstract class InAppPurchaseApi { /// Wraps BillingClient#endConnection(BillingClientStateListener). void endConnection(); + /// Wraps BillingClient#launchBillingFlow(Activity, BillingFlowParams). + PlatformBillingResult launchBillingFlow(PlatformBillingFlowParams params); + /// Wraps BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener). @async PlatformBillingResult acknowledgePurchase(String purchaseToken); diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index 7b27b6012c8..6d4ade8cc57 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -199,18 +199,13 @@ void main() { }); group('launchBillingFlow', () { - const String launchMethodName = - 'BillingClient#launchBillingFlow(Activity, BillingFlowParams)'; - test('serializes and deserializes data', () async { const String debugMessage = 'dummy message'; const BillingResponse responseCode = BillingResponse.ok; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - ); + when(mockApi.launchBillingFlow(any)).thenAnswer( + (_) async => convertToPigeonResult(expectedBillingResult)); const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; const String profileId = 'hashedProfileId'; @@ -221,25 +216,19 @@ void main() { accountId: accountId, obfuscatedProfileId: profileId), equals(expectedBillingResult)); - final Map arguments = stubPlatform - .previousCallMatching(launchMethodName) - .arguments as Map; - expect(arguments['product'], equals(productDetails.productId)); - expect(arguments['accountId'], equals(accountId)); - expect(arguments['obfuscatedProfileId'], equals(profileId)); + + final VerificationResult result = + verify(mockApi.launchBillingFlow(captureAny)); + final PlatformBillingFlowParams params = + result.captured.single as PlatformBillingFlowParams; + expect(params.product, equals(productDetails.productId)); + expect(params.accountId, equals(accountId)); + expect(params.obfuscatedProfileId, equals(profileId)); }); test( 'Change subscription throws assertion error `oldProduct` and `purchaseToken` has different nullability', () async { - const String debugMessage = 'dummy message'; - const BillingResponse responseCode = BillingResponse.ok; - const BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - ); const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; const String profileId = 'hashedProfileId'; @@ -268,10 +257,8 @@ void main() { const BillingResponse responseCode = BillingResponse.ok; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - ); + when(mockApi.launchBillingFlow(any)).thenAnswer( + (_) async => convertToPigeonResult(expectedBillingResult)); const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; const String profileId = 'hashedProfileId'; @@ -284,15 +271,15 @@ void main() { oldProduct: dummyOldPurchase.products.first, purchaseToken: dummyOldPurchase.purchaseToken), equals(expectedBillingResult)); - final Map arguments = stubPlatform - .previousCallMatching(launchMethodName) - .arguments as Map; - expect(arguments['product'], equals(productDetails.productId)); - expect(arguments['accountId'], equals(accountId)); - expect(arguments['oldProduct'], equals(dummyOldPurchase.products.first)); - expect( - arguments['purchaseToken'], equals(dummyOldPurchase.purchaseToken)); - expect(arguments['obfuscatedProfileId'], equals(profileId)); + final VerificationResult result = + verify(mockApi.launchBillingFlow(captureAny)); + final PlatformBillingFlowParams params = + result.captured.single as PlatformBillingFlowParams; + expect(params.product, equals(productDetails.productId)); + expect(params.accountId, equals(accountId)); + expect(params.oldProduct, equals(dummyOldPurchase.products.first)); + expect(params.purchaseToken, equals(dummyOldPurchase.purchaseToken)); + expect(params.obfuscatedProfileId, equals(profileId)); }); test( @@ -302,10 +289,8 @@ void main() { const BillingResponse responseCode = BillingResponse.ok; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - ); + when(mockApi.launchBillingFlow(any)).thenAnswer( + (_) async => convertToPigeonResult(expectedBillingResult)); const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; const String profileId = 'hashedProfileId'; @@ -321,16 +306,16 @@ void main() { prorationMode: prorationMode, purchaseToken: dummyOldPurchase.purchaseToken), equals(expectedBillingResult)); - final Map arguments = stubPlatform - .previousCallMatching(launchMethodName) - .arguments as Map; - expect(arguments['product'], equals(productDetails.productId)); - expect(arguments['accountId'], equals(accountId)); - expect(arguments['oldProduct'], equals(dummyOldPurchase.products.first)); - expect(arguments['obfuscatedProfileId'], equals(profileId)); - expect( - arguments['purchaseToken'], equals(dummyOldPurchase.purchaseToken)); - expect(arguments['prorationMode'], + final VerificationResult result = + verify(mockApi.launchBillingFlow(captureAny)); + final PlatformBillingFlowParams params = + result.captured.single as PlatformBillingFlowParams; + expect(params.product, equals(productDetails.productId)); + expect(params.accountId, equals(accountId)); + expect(params.oldProduct, equals(dummyOldPurchase.products.first)); + expect(params.obfuscatedProfileId, equals(profileId)); + expect(params.purchaseToken, equals(dummyOldPurchase.purchaseToken)); + expect(params.prorationMode, const ProrationModeConverter().toJson(prorationMode)); }); @@ -341,10 +326,8 @@ void main() { const BillingResponse responseCode = BillingResponse.ok; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - ); + when(mockApi.launchBillingFlow(any)).thenAnswer( + (_) async => convertToPigeonResult(expectedBillingResult)); const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; const String profileId = 'hashedProfileId'; @@ -360,16 +343,16 @@ void main() { prorationMode: prorationMode, purchaseToken: dummyOldPurchase.purchaseToken), equals(expectedBillingResult)); - final Map arguments = stubPlatform - .previousCallMatching(launchMethodName) - .arguments as Map; - expect(arguments['product'], equals(productDetails.productId)); - expect(arguments['accountId'], equals(accountId)); - expect(arguments['oldProduct'], equals(dummyOldPurchase.products.first)); - expect(arguments['obfuscatedProfileId'], equals(profileId)); - expect( - arguments['purchaseToken'], equals(dummyOldPurchase.purchaseToken)); - expect(arguments['prorationMode'], + final VerificationResult result = + verify(mockApi.launchBillingFlow(captureAny)); + final PlatformBillingFlowParams params = + result.captured.single as PlatformBillingFlowParams; + expect(params.product, equals(productDetails.productId)); + expect(params.accountId, equals(accountId)); + expect(params.oldProduct, equals(dummyOldPurchase.products.first)); + expect(params.obfuscatedProfileId, equals(profileId)); + expect(params.purchaseToken, equals(dummyOldPurchase.purchaseToken)); + expect(params.prorationMode, const ProrationModeConverter().toJson(prorationMode)); }); @@ -378,34 +361,20 @@ void main() { const BillingResponse responseCode = BillingResponse.ok; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - ); + when(mockApi.launchBillingFlow(any)).thenAnswer( + (_) async => convertToPigeonResult(expectedBillingResult)); const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; expect( await billingClient.launchBillingFlow( product: productDetails.productId), equals(expectedBillingResult)); - final Map arguments = stubPlatform - .previousCallMatching(launchMethodName) - .arguments as Map; - expect(arguments['product'], equals(productDetails.productId)); - expect(arguments['accountId'], isNull); - }); - - test('handles method channel returning null', () async { - stubPlatform.addResponse( - name: launchMethodName, - ); - const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; - expect( - await billingClient.launchBillingFlow( - product: productDetails.productId), - equals(const BillingResultWrapper( - responseCode: BillingResponse.error, - debugMessage: kInvalidBillingResultErrorMessage))); + final VerificationResult result = + verify(mockApi.launchBillingFlow(captureAny)); + final PlatformBillingFlowParams params = + result.captured.single as PlatformBillingFlowParams; + expect(params.product, equals(productDetails.productId)); + expect(params.accountId, isNull); }); }); diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart index a52081a5c60..65ffdbe23a1 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart @@ -93,6 +93,32 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { returnValueForMissingStub: _i3.Future.value(), ) as _i3.Future); + @override + _i3.Future<_i2.PlatformBillingResult> launchBillingFlow( + _i2.PlatformBillingFlowParams? params) => + (super.noSuchMethod( + Invocation.method( + #launchBillingFlow, + [params], + ), + returnValue: _i3.Future<_i2.PlatformBillingResult>.value( + _FakePlatformBillingResult_0( + this, + Invocation.method( + #launchBillingFlow, + [params], + ), + )), + returnValueForMissingStub: _i3.Future<_i2.PlatformBillingResult>.value( + _FakePlatformBillingResult_0( + this, + Invocation.method( + #launchBillingFlow, + [params], + ), + )), + ) as _i3.Future<_i2.PlatformBillingResult>); + @override _i3.Future<_i2.PlatformBillingResult> acknowledgePurchase( String? purchaseToken) => 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 fb1bb972d27..ef5ebf9e07c 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 @@ -321,9 +321,6 @@ void main() { }); group('make payment', () { - const String launchMethodName = - 'BillingClient#launchBillingFlow(Activity, BillingFlowParams)'; - test('buy non consumable, serializes and deserializes data', () async { const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; @@ -332,33 +329,32 @@ void main() { const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: sentCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - additionalStepBeforeReturn: (dynamic _) { - // Mock java update purchase callback. - final MethodCall call = - MethodCall(kOnPurchasesUpdated, { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const BillingResponseConverter().toJson(sentCode), - 'purchasesList': [ - { - 'orderId': 'orderID1', - 'products': [productDetails.productId], - 'isAutoRenewing': false, - 'packageName': 'package', - 'purchaseTime': 1231231231, - 'purchaseToken': 'token', - 'signature': 'sign', - 'originalJson': 'json', - 'developerPayload': 'dummy payload', - 'isAcknowledged': true, - 'purchaseState': 1, - } - ] - }); - iapAndroidPlatform.billingClientManager.client.callHandler(call); - }); + when(mockApi.launchBillingFlow(any)).thenAnswer((_) async { + // Mock java update purchase callback. + final MethodCall call = + MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': const BillingResponseConverter().toJson(sentCode), + 'purchasesList': [ + { + 'orderId': 'orderID1', + 'products': [productDetails.productId], + 'isAutoRenewing': false, + 'packageName': 'package', + 'purchaseTime': 1231231231, + 'purchaseToken': 'token', + 'signature': 'sign', + 'originalJson': 'json', + 'developerPayload': 'dummy payload', + 'isAcknowledged': true, + 'purchaseState': 1, + } + ] + }); + await iapAndroidPlatform.billingClientManager.client.callHandler(call); + + return convertToPigeonResult(expectedBillingResult); + }); final Completer completer = Completer(); PurchaseDetails purchaseDetails; final Stream> purchaseStream = @@ -391,19 +387,18 @@ void main() { const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: sentCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - additionalStepBeforeReturn: (dynamic _) { - // Mock java update purchase callback. - final MethodCall call = - MethodCall(kOnPurchasesUpdated, { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const BillingResponseConverter().toJson(sentCode), - 'purchasesList': const [] - }); - iapAndroidPlatform.billingClientManager.client.callHandler(call); - }); + when(mockApi.launchBillingFlow(any)).thenAnswer((_) async { + // Mock java update purchase callback. + final MethodCall call = + MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': const BillingResponseConverter().toJson(sentCode), + 'purchasesList': const [] + }); + await iapAndroidPlatform.billingClientManager.client.callHandler(call); + + return convertToPigeonResult(expectedBillingResult); + }); final Completer completer = Completer(); PurchaseDetails purchaseDetails; final Stream> purchaseStream = @@ -436,33 +431,32 @@ void main() { const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: sentCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - additionalStepBeforeReturn: (dynamic _) { - // Mock java update purchase callback. - final MethodCall call = - MethodCall(kOnPurchasesUpdated, { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const BillingResponseConverter().toJson(sentCode), - 'purchasesList': [ - { - 'orderId': 'orderID1', - 'products': [productDetails.productId], - 'isAutoRenewing': false, - 'packageName': 'package', - 'purchaseTime': 1231231231, - 'purchaseToken': 'token', - 'signature': 'sign', - 'originalJson': 'json', - 'developerPayload': 'dummy payload', - 'isAcknowledged': true, - 'purchaseState': 1, - } - ] - }); - iapAndroidPlatform.billingClientManager.client.callHandler(call); - }); + when(mockApi.launchBillingFlow(any)).thenAnswer((_) async { + // Mock java update purchase callback. + final MethodCall call = + MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': const BillingResponseConverter().toJson(sentCode), + 'purchasesList': [ + { + 'orderId': 'orderID1', + 'products': [productDetails.productId], + 'isAutoRenewing': false, + 'packageName': 'package', + 'purchaseTime': 1231231231, + 'purchaseToken': 'token', + 'signature': 'sign', + 'originalJson': 'json', + 'developerPayload': 'dummy payload', + 'isAcknowledged': true, + 'purchaseState': 1, + } + ] + }); + await iapAndroidPlatform.billingClientManager.client.callHandler(call); + + return convertToPigeonResult(expectedBillingResult); + }); final Completer consumeCompleter = Completer(); // adding call back for consume purchase const BillingResponse expectedCode = BillingResponse.ok; @@ -510,9 +504,8 @@ void main() { const BillingResponse sentCode = BillingResponse.error; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: sentCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult)); + when(mockApi.launchBillingFlow(any)).thenAnswer( + (_) async => convertToPigeonResult(expectedBillingResult)); final bool result = await iapAndroidPlatform.buyNonConsumable( purchaseParam: GooglePlayPurchaseParam( @@ -530,10 +523,8 @@ void main() { const BillingResponse sentCode = BillingResponse.developerError; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: sentCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - ); + when(mockApi.launchBillingFlow(any)).thenAnswer( + (_) async => convertToPigeonResult(expectedBillingResult)); final bool result = await iapAndroidPlatform.buyConsumable( purchaseParam: GooglePlayPurchaseParam( @@ -552,33 +543,32 @@ void main() { const BillingResponse sentCode = BillingResponse.ok; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: sentCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - additionalStepBeforeReturn: (dynamic _) { - // Mock java update purchase callback. - final MethodCall call = - MethodCall(kOnPurchasesUpdated, { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const BillingResponseConverter().toJson(sentCode), - 'purchasesList': [ - { - 'orderId': 'orderID1', - 'products': [productDetails.productId], - 'isAutoRenewing': false, - 'packageName': 'package', - 'purchaseTime': 1231231231, - 'purchaseToken': 'token', - 'signature': 'sign', - 'originalJson': 'json', - 'developerPayload': 'dummy payload', - 'isAcknowledged': true, - 'purchaseState': 1, - } - ] - }); - iapAndroidPlatform.billingClientManager.client.callHandler(call); - }); + when(mockApi.launchBillingFlow(any)).thenAnswer((_) async { + // Mock java update purchase callback. + final MethodCall call = + MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': const BillingResponseConverter().toJson(sentCode), + 'purchasesList': [ + { + 'orderId': 'orderID1', + 'products': [productDetails.productId], + 'isAutoRenewing': false, + 'packageName': 'package', + 'purchaseTime': 1231231231, + 'purchaseToken': 'token', + 'signature': 'sign', + 'originalJson': 'json', + 'developerPayload': 'dummy payload', + 'isAcknowledged': true, + 'purchaseState': 1, + } + ] + }); + await iapAndroidPlatform.billingClientManager.client.callHandler(call); + + return convertToPigeonResult(expectedBillingResult); + }); final Completer consumeCompleter = Completer(); // adding call back for consume purchase const BillingResponse expectedCode = BillingResponse.error; @@ -629,33 +619,32 @@ void main() { const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: sentCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - additionalStepBeforeReturn: (dynamic _) { - // Mock java update purchase callback. - final MethodCall call = - MethodCall(kOnPurchasesUpdated, { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const BillingResponseConverter().toJson(sentCode), - 'purchasesList': [ - { - 'orderId': 'orderID1', - 'products': [productDetails.productId], - 'isAutoRenewing': false, - 'packageName': 'package', - 'purchaseTime': 1231231231, - 'purchaseToken': 'token', - 'signature': 'sign', - 'originalJson': 'json', - 'developerPayload': 'dummy payload', - 'isAcknowledged': true, - 'purchaseState': 1, - } - ] - }); - iapAndroidPlatform.billingClientManager.client.callHandler(call); - }); + when(mockApi.launchBillingFlow(any)).thenAnswer((_) async { + // Mock java update purchase callback. + final MethodCall call = + MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': const BillingResponseConverter().toJson(sentCode), + 'purchasesList': [ + { + 'orderId': 'orderID1', + 'products': [productDetails.productId], + 'isAutoRenewing': false, + 'packageName': 'package', + 'purchaseTime': 1231231231, + 'purchaseToken': 'token', + 'signature': 'sign', + 'originalJson': 'json', + 'developerPayload': 'dummy payload', + 'isAcknowledged': true, + 'purchaseState': 1, + } + ] + }); + await iapAndroidPlatform.billingClientManager.client.callHandler(call); + + return convertToPigeonResult(expectedBillingResult); + }); final Completer consumeCompleter = Completer(); // adding call back for consume purchase const BillingResponse expectedCode = BillingResponse.ok; @@ -694,33 +683,32 @@ void main() { const BillingResponse sentCode = BillingResponse.userCanceled; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: sentCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - additionalStepBeforeReturn: (dynamic _) { - // Mock java update purchase callback. - final MethodCall call = - MethodCall(kOnPurchasesUpdated, { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const BillingResponseConverter().toJson(sentCode), - 'purchasesList': [ - { - 'orderId': 'orderID1', - 'products': [productDetails.productId], - 'isAutoRenewing': false, - 'packageName': 'package', - 'purchaseTime': 1231231231, - 'purchaseToken': 'token', - 'signature': 'sign', - 'originalJson': 'json', - 'developerPayload': 'dummy payload', - 'isAcknowledged': true, - 'purchaseState': 1, - } - ] - }); - iapAndroidPlatform.billingClientManager.client.callHandler(call); - }); + when(mockApi.launchBillingFlow(any)).thenAnswer((_) async { + // Mock java update purchase callback. + final MethodCall call = + MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': const BillingResponseConverter().toJson(sentCode), + 'purchasesList': [ + { + 'orderId': 'orderID1', + 'products': [productDetails.productId], + 'isAutoRenewing': false, + 'packageName': 'package', + 'purchaseTime': 1231231231, + 'purchaseToken': 'token', + 'signature': 'sign', + 'originalJson': 'json', + 'developerPayload': 'dummy payload', + 'isAcknowledged': true, + 'purchaseState': 1, + } + ] + }); + await iapAndroidPlatform.billingClientManager.client.callHandler(call); + + return convertToPigeonResult(expectedBillingResult); + }); final Completer consumeCompleter = Completer(); // adding call back for consume purchase const BillingResponse expectedCode = BillingResponse.userCanceled; @@ -765,19 +753,18 @@ void main() { const BillingResponse sentCode = BillingResponse.ok; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: sentCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - additionalStepBeforeReturn: (dynamic _) { - // Mock java update purchase callback. - final MethodCall call = - MethodCall(kOnPurchasesUpdated, { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const BillingResponseConverter().toJson(sentCode), - 'purchasesList': const [] - }); - iapAndroidPlatform.billingClientManager.client.callHandler(call); - }); + when(mockApi.launchBillingFlow(any)).thenAnswer((_) async { + // Mock java update purchase callback. + final MethodCall call = + MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': const BillingResponseConverter().toJson(sentCode), + 'purchasesList': const [] + }); + await iapAndroidPlatform.billingClientManager.client.callHandler(call); + + return convertToPigeonResult(expectedBillingResult); + }); final Completer completer = Completer(); PurchaseDetails purchaseDetails; From 8ab863bd797a5d1f3873990d3589ad7125ed80e0 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 5 Mar 2024 15:00:01 -0500 Subject: [PATCH 13/25] Naming consistency --- .../plugins/inapppurchase/Messages.java | 6 +-- .../inapppurchase/MethodCallHandlerImpl.java | 2 +- .../inapppurchase/MethodCallHandlerTest.java | 5 +- .../billing_client_wrapper.dart | 2 +- .../lib/src/messages.g.dart | 4 +- .../pigeons/messages.dart | 2 +- .../billing_client_wrapper_test.dart | 4 +- .../billing_client_wrapper_test.mocks.dart | 47 ++++++++++--------- ...rchase_android_platform_addition_test.dart | 6 +-- 9 files changed, 40 insertions(+), 38 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java index 4b5e90609dc..ca638e5b688 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -440,7 +440,7 @@ void acknowledgePurchase( @NonNull Boolean isFeatureSupported(@NonNull String feature); /** Wraps BillingClient#isAlternativeBillingOnlyAvailableAsync(). */ - void isAlternativeBillingOnlyAvailable(@NonNull Result result); + void isAlternativeBillingOnlyAvailableAsync(@NonNull Result result); /** Wraps BillingClient#showAlternativeBillingOnlyInformationDialog(). */ void showAlternativeBillingOnlyInformationDialog(@NonNull Result result); @@ -650,7 +650,7 @@ public void error(Throwable error) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.isAlternativeBillingOnlyAvailable", + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.isAlternativeBillingOnlyAvailableAsync", getCodec()); if (api != null) { channel.setMessageHandler( @@ -669,7 +669,7 @@ public void error(Throwable error) { } }; - api.isAlternativeBillingOnlyAvailable(resultCallback); + api.isAlternativeBillingOnlyAvailableAsync(resultCallback); }); } else { channel.setMessageHandler(null); diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index 66048168eb6..c2a3a6d9e75 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -188,7 +188,7 @@ private void createAlternativeBillingOnlyReportingDetails(final MethodChannel.Re } @Override - public void isAlternativeBillingOnlyAvailable( + public void isAlternativeBillingOnlyAvailableAsync( @NonNull Messages.Result result) { validateBillingClient(); assert billingClient != null; diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index af2cdefc2d3..64eafef5b19 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -331,7 +331,7 @@ public void isAlternativeBillingOnlyAvailableSuccess() { .when(mockBillingClient) .isAlternativeBillingOnlyAvailableAsync(listenerCaptor.capture()); - methodChannelHandler.isAlternativeBillingOnlyAvailable(platformBillingResult); + methodChannelHandler.isAlternativeBillingOnlyAvailableAsync(platformBillingResult); listenerCaptor.getValue().onAlternativeBillingOnlyAvailabilityResponse(billingResult); ArgumentCaptor resultCaptor = @@ -345,7 +345,8 @@ public void isAlternativeBillingOnlyAvailable_serviceDisconnected() { Messages.FlutterError exception = assertThrows( Messages.FlutterError.class, - () -> methodChannelHandler.isAlternativeBillingOnlyAvailable(platformBillingResult)); + () -> + methodChannelHandler.isAlternativeBillingOnlyAvailableAsync(platformBillingResult)); assertEquals("UNAVAILABLE", exception.code); assertTrue(Objects.requireNonNull(exception.getMessage()).contains("BillingClient")); } 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 17fd816a370..2163e98f65f 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 @@ -328,7 +328,7 @@ class BillingClient { /// Checks if "AlterntitiveBillingOnly" feature is available. Future isAlternativeBillingOnlyAvailable() async { return resultWrapperFromPlatform( - await _hostApi.isAlternativeBillingOnlyAvailable()); + await _hostApi.isAlternativeBillingOnlyAvailableAsync()); } /// Shows the alternative billing only information dialog on top of the calling app. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart index 87836f98ce4..5786bae49b5 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -356,9 +356,9 @@ class InAppPurchaseApi { } /// Wraps BillingClient#isAlternativeBillingOnlyAvailableAsync(). - Future isAlternativeBillingOnlyAvailable() async { + Future isAlternativeBillingOnlyAvailableAsync() async { const String __pigeon_channelName = - 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.isAlternativeBillingOnlyAvailable'; + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.isAlternativeBillingOnlyAvailableAsync'; final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart index 040c6329932..ff043eb1bfb 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -87,7 +87,7 @@ abstract class InAppPurchaseApi { /// Wraps BillingClient#isAlternativeBillingOnlyAvailableAsync(). @async - PlatformBillingResult isAlternativeBillingOnlyAvailable(); + PlatformBillingResult isAlternativeBillingOnlyAvailableAsync(); /// Wraps BillingClient#showAlternativeBillingOnlyInformationDialog(). @async diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index 6d4ade8cc57..eaab8ceef36 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -595,8 +595,8 @@ void main() { test('returns object', () async { const BillingResultWrapper expected = BillingResultWrapper( responseCode: BillingResponse.ok, debugMessage: 'message'); - when(mockApi.isAlternativeBillingOnlyAvailable()).thenAnswer((_) async => - PlatformBillingResult( + when(mockApi.isAlternativeBillingOnlyAvailableAsync()).thenAnswer( + (_) async => PlatformBillingResult( responseCode: 0, debugMessage: expected.debugMessage!)); final BillingResultWrapper result = await billingClient.isAlternativeBillingOnlyAvailable(); diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart index 65ffdbe23a1..80b8a3ed3af 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart @@ -181,29 +181,30 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { ) as _i3.Future); @override - _i3.Future<_i2.PlatformBillingResult> isAlternativeBillingOnlyAvailable() => - (super.noSuchMethod( - Invocation.method( - #isAlternativeBillingOnlyAvailable, - [], - ), - returnValue: _i3.Future<_i2.PlatformBillingResult>.value( - _FakePlatformBillingResult_0( - this, - Invocation.method( - #isAlternativeBillingOnlyAvailable, - [], - ), - )), - returnValueForMissingStub: _i3.Future<_i2.PlatformBillingResult>.value( - _FakePlatformBillingResult_0( - this, - Invocation.method( - #isAlternativeBillingOnlyAvailable, - [], - ), - )), - ) as _i3.Future<_i2.PlatformBillingResult>); + _i3.Future<_i2.PlatformBillingResult> + isAlternativeBillingOnlyAvailableAsync() => (super.noSuchMethod( + Invocation.method( + #isAlternativeBillingOnlyAvailableAsync, + [], + ), + returnValue: _i3.Future<_i2.PlatformBillingResult>.value( + _FakePlatformBillingResult_0( + this, + Invocation.method( + #isAlternativeBillingOnlyAvailableAsync, + [], + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.PlatformBillingResult>.value( + _FakePlatformBillingResult_0( + this, + Invocation.method( + #isAlternativeBillingOnlyAvailableAsync, + [], + ), + )), + ) as _i3.Future<_i2.PlatformBillingResult>); @override _i3.Future<_i2.PlatformBillingResult> 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 1d2458e87ac..4cb595098c2 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 @@ -122,8 +122,8 @@ void main() { test('isAlternativeBillingOnlyAvailable success', () async { const BillingResultWrapper expected = BillingResultWrapper( responseCode: BillingResponse.ok, debugMessage: 'dummy message'); - when(mockApi.isAlternativeBillingOnlyAvailable()).thenAnswer((_) async => - PlatformBillingResult( + when(mockApi.isAlternativeBillingOnlyAvailableAsync()).thenAnswer( + (_) async => PlatformBillingResult( responseCode: 0, debugMessage: expected.debugMessage!)); final BillingResultWrapper result = @@ -138,7 +138,7 @@ void main() { const BillingResultWrapper expected = BillingResultWrapper( responseCode: BillingResponse.ok, debugMessage: 'dummy message'); - when(mockApi.isAlternativeBillingOnlyAvailable()) + when(mockApi.isAlternativeBillingOnlyAvailableAsync()) .thenAnswer((_) async => convertToPigeonResult(expected)); when(mockApi.showAlternativeBillingOnlyInformationDialog()) .thenAnswer((_) async => convertToPigeonResult(expected)); From b40e7929dc178e3efd794d93ff8df83342b8e27d Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 6 Mar 2024 12:50:16 -0500 Subject: [PATCH 14/25] Convert queryProductDetailsAsync --- .../plugins/inapppurchase/Messages.java | 227 ++++++++++++++++++ .../inapppurchase/MethodCallHandlerImpl.java | 56 ++--- .../plugins/inapppurchase/Translator.java | 37 +-- .../inapppurchase/MethodCallHandlerTest.java | 70 +++--- .../plugins/inapppurchase/TranslatorTest.java | 11 +- .../billing_client_wrapper.dart | 15 +- .../lib/src/messages.g.dart | 104 ++++++++ .../lib/src/pigeon_converters.dart | 30 ++- .../pigeons/messages.dart | 43 ++++ .../billing_client_wrapper_test.dart | 55 ++--- .../billing_client_wrapper_test.mocks.dart | 38 +++ ...in_app_purchase_android_platform_test.dart | 77 +++--- 12 files changed, 591 insertions(+), 172 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java index ca638e5b688..f1796e38368 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -21,6 +21,7 @@ import java.lang.annotation.Target; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.List; /** Generated class from Pigeon. */ @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) @@ -63,6 +64,18 @@ protected static ArrayList wrapError(@NonNull Throwable exception) { @Retention(CLASS) @interface CanIgnoreReturnValue {} + /** Pigeon version of Java BillingClient.ProductType. */ + public enum PlatformProductType { + INAPP(0), + SUBS(1); + + final int index; + + private PlatformProductType(final int index) { + this.index = index; + } + } + /** Pigeon version of billing_client_wrapper.dart's BillingChoiceMode. */ public enum PlatformBillingChoiceMode { /** @@ -81,6 +94,85 @@ private PlatformBillingChoiceMode(final int index) { } } + /** + * Pigeon version of Java Product. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PlatformProduct { + private @NonNull String productId; + + public @NonNull String getProductId() { + return productId; + } + + public void setProductId(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"productId\" is null."); + } + this.productId = setterArg; + } + + private @NonNull PlatformProductType productType; + + public @NonNull PlatformProductType getProductType() { + return productType; + } + + public void setProductType(@NonNull PlatformProductType setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"productType\" is null."); + } + this.productType = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PlatformProduct() {} + + public static final class Builder { + + private @Nullable String productId; + + @CanIgnoreReturnValue + public @NonNull Builder setProductId(@NonNull String setterArg) { + this.productId = setterArg; + return this; + } + + private @Nullable PlatformProductType productType; + + @CanIgnoreReturnValue + public @NonNull Builder setProductType(@NonNull PlatformProductType setterArg) { + this.productType = setterArg; + return this; + } + + public @NonNull PlatformProduct build() { + PlatformProduct pigeonReturn = new PlatformProduct(); + pigeonReturn.setProductId(productId); + pigeonReturn.setProductType(productType); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(productId); + toListResult.add(productType == null ? null : productType.index); + return toListResult; + } + + static @NonNull PlatformProduct fromList(@NonNull ArrayList list) { + PlatformProduct pigeonResult = new PlatformProduct(); + Object productId = list.get(0); + pigeonResult.setProductId((String) productId); + Object productType = list.get(1); + pigeonResult.setProductType(PlatformProductType.values()[(int) productType]); + return pigeonResult; + } + } + /** * Pigeon version of Java BillingResult. * @@ -163,6 +255,93 @@ ArrayList toList() { } } + /** + * Pigeon version of ProductDetailsResponseWrapper, which contains the components of the java + * ProductDetailsResponseListener callback. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PlatformProductDetailsResponse { + private @NonNull PlatformBillingResult billingResult; + + public @NonNull PlatformBillingResult getBillingResult() { + return billingResult; + } + + public void setBillingResult(@NonNull PlatformBillingResult setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"billingResult\" is null."); + } + this.billingResult = setterArg; + } + + /** + * A JSON-compatible list of details, where each entry in the list is a Map + * JSON encoding of the product details. + */ + private @NonNull List productDetailsJsonList; + + public @NonNull List getProductDetailsJsonList() { + return productDetailsJsonList; + } + + public void setProductDetailsJsonList(@NonNull List setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"productDetailsJsonList\" is null."); + } + this.productDetailsJsonList = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PlatformProductDetailsResponse() {} + + public static final class Builder { + + private @Nullable PlatformBillingResult billingResult; + + @CanIgnoreReturnValue + public @NonNull Builder setBillingResult(@NonNull PlatformBillingResult setterArg) { + this.billingResult = setterArg; + return this; + } + + private @Nullable List productDetailsJsonList; + + @CanIgnoreReturnValue + public @NonNull Builder setProductDetailsJsonList(@NonNull List setterArg) { + this.productDetailsJsonList = setterArg; + return this; + } + + public @NonNull PlatformProductDetailsResponse build() { + PlatformProductDetailsResponse pigeonReturn = new PlatformProductDetailsResponse(); + pigeonReturn.setBillingResult(billingResult); + pigeonReturn.setProductDetailsJsonList(productDetailsJsonList); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add((billingResult == null) ? null : billingResult.toList()); + toListResult.add(productDetailsJsonList); + return toListResult; + } + + static @NonNull PlatformProductDetailsResponse fromList(@NonNull ArrayList list) { + PlatformProductDetailsResponse pigeonResult = new PlatformProductDetailsResponse(); + Object billingResult = list.get(0); + pigeonResult.setBillingResult( + (billingResult == null) + ? null + : PlatformBillingResult.fromList((ArrayList) billingResult)); + Object productDetailsJsonList = list.get(1); + pigeonResult.setProductDetailsJsonList((List) productDetailsJsonList); + return pigeonResult; + } + } + /** * Pigeon version of Java BillingFlowParams. * @@ -394,6 +573,10 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { return PlatformBillingFlowParams.fromList((ArrayList) readValue(buffer)); case (byte) 129: return PlatformBillingResult.fromList((ArrayList) readValue(buffer)); + case (byte) 130: + return PlatformProduct.fromList((ArrayList) readValue(buffer)); + case (byte) 131: + return PlatformProductDetailsResponse.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); } @@ -407,6 +590,12 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { } else if (value instanceof PlatformBillingResult) { stream.write(129); writeValue(stream, ((PlatformBillingResult) value).toList()); + } else if (value instanceof PlatformProduct) { + stream.write(130); + writeValue(stream, ((PlatformProduct) value).toList()); + } else if (value instanceof PlatformProductDetailsResponse) { + stream.write(131); + writeValue(stream, ((PlatformProductDetailsResponse) value).toList()); } else { super.writeValue(stream, value); } @@ -436,6 +625,13 @@ void acknowledgePurchase( @NonNull String purchaseToken, @NonNull Result result); /** Wraps BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener). */ void consumeAsync(@NonNull String purchaseToken, @NonNull Result result); + /** + * Wraps BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, + * ProductDetailsResponseListener). + */ + void queryProductDetailsAsync( + @NonNull List products, + @NonNull Result result); /** Wraps BillingClient#isFeatureSupported(String). */ @NonNull Boolean isFeatureSupported(@NonNull String feature); @@ -621,6 +817,37 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.queryProductDetailsAsync", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + List productsArg = (List) args.get(0); + Result resultCallback = + new Result() { + public void success(PlatformProductDetailsResponse result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.queryProductDetailsAsync(productsArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } { BasicMessageChannel channel = new BasicMessageChannel<>( diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index c2a3a6d9e75..5a34dd5f90e 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -4,8 +4,6 @@ package io.flutter.plugins.inapppurchase; -import static io.flutter.plugins.inapppurchase.Messages.FlutterError; -import static io.flutter.plugins.inapppurchase.Messages.InAppPurchaseApi; import static io.flutter.plugins.inapppurchase.Translator.fromAlternativeBillingOnlyReportingDetails; import static io.flutter.plugins.inapppurchase.Translator.fromBillingConfig; import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult; @@ -33,11 +31,17 @@ import com.android.billingclient.api.GetBillingConfigParams; import com.android.billingclient.api.ProductDetails; import com.android.billingclient.api.QueryProductDetailsParams; -import com.android.billingclient.api.QueryProductDetailsParams.Product; import com.android.billingclient.api.QueryPurchaseHistoryParams; import com.android.billingclient.api.QueryPurchasesParams; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.inapppurchase.Messages.FlutterError; +import io.flutter.plugins.inapppurchase.Messages.InAppPurchaseApi; +import io.flutter.plugins.inapppurchase.Messages.PlatformBillingChoiceMode; +import io.flutter.plugins.inapppurchase.Messages.PlatformBillingFlowParams; +import io.flutter.plugins.inapppurchase.Messages.PlatformBillingResult; +import io.flutter.plugins.inapppurchase.Messages.PlatformProduct; +import io.flutter.plugins.inapppurchase.Messages.PlatformProductDetailsResponse; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -52,8 +56,6 @@ class MethodCallHandlerImpl @VisibleForTesting static final class MethodNames { static final String ON_DISCONNECT = "BillingClientStateListener#onBillingServiceDisconnected()"; - static final String QUERY_PRODUCT_DETAILS = - "BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener)"; static final String QUERY_PURCHASES_ASYNC = "BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)"; static final String QUERY_PURCHASE_HISTORY_ASYNC = @@ -141,10 +143,6 @@ void onDetachedFromActivity() { @Override public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { switch (call.method) { - case MethodNames.QUERY_PRODUCT_DETAILS: - List productList = toProductList(call.argument("productList")); - queryProductDetailsAsync(productList, result); - break; case MethodNames.QUERY_PURCHASES_ASYNC: queryPurchasesAsync((String) call.argument("productType"), result); break; @@ -164,7 +162,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result @Override public void showAlternativeBillingOnlyInformationDialog( - @NonNull Messages.Result result) { + @NonNull Messages.Result result) { validateBillingClient(); assert billingClient != null; if (activity == null) { @@ -189,7 +187,7 @@ private void createAlternativeBillingOnlyReportingDetails(final MethodChannel.Re @Override public void isAlternativeBillingOnlyAvailableAsync( - @NonNull Messages.Result result) { + @NonNull Messages.Result result) { validateBillingClient(); assert billingClient != null; billingClient.isAlternativeBillingOnlyAvailableAsync( @@ -227,30 +225,30 @@ public Boolean isReady() { return billingClient.isReady(); } - private void queryProductDetailsAsync( - final List productList, final MethodChannel.Result result) { - if (billingClientError(result)) { - return; - } + @Override + public void queryProductDetailsAsync( + @NonNull List products, + @NonNull Messages.Result result) { + validateBillingClient(); assert billingClient != null; QueryProductDetailsParams params = - QueryProductDetailsParams.newBuilder().setProductList(productList).build(); + QueryProductDetailsParams.newBuilder().setProductList(toProductList(products)).build(); billingClient.queryProductDetailsAsync( params, (billingResult, productDetailsList) -> { updateCachedProducts(productDetailsList); - final Map productDetailsResponse = new HashMap<>(); - productDetailsResponse.put("billingResult", fromBillingResult(billingResult)); - productDetailsResponse.put( - "productDetailsList", fromProductDetailsList(productDetailsList)); - result.success(productDetailsResponse); + final PlatformProductDetailsResponse.Builder responseBuilder = + new PlatformProductDetailsResponse.Builder() + .setBillingResult(pigeonBillingResultFromBillingResult(billingResult)) + .setProductDetailsJsonList(fromProductDetailsList(productDetailsList)); + result.success(responseBuilder.build()); }); } @Override - public @NonNull Messages.PlatformBillingResult launchBillingFlow( - @NonNull Messages.PlatformBillingFlowParams params) { + public @NonNull PlatformBillingResult launchBillingFlow( + @NonNull PlatformBillingFlowParams params) { validateBillingClient(); assert billingClient != null; @@ -363,8 +361,7 @@ private void setReplaceProrationMode( @Override public void consumeAsync( - @NonNull String purchaseToken, - @NonNull Messages.Result result) { + @NonNull String purchaseToken, @NonNull Messages.Result result) { validateBillingClient(); assert billingClient != null; @@ -420,8 +417,8 @@ private void queryPurchaseHistoryAsync(String productType, final MethodChannel.R @Override public void startConnection( @NonNull Long handle, - @NonNull Messages.PlatformBillingChoiceMode billingMode, - @NonNull Messages.Result result) { + @NonNull PlatformBillingChoiceMode billingMode, + @NonNull Messages.Result result) { if (billingClient == null) { billingClient = billingClientFactory.createBillingClient(applicationContext, methodChannel, billingMode); @@ -454,8 +451,7 @@ public void onBillingServiceDisconnected() { @Override public void acknowledgePurchase( - @NonNull String purchaseToken, - @NonNull Messages.Result result) { + @NonNull String purchaseToken, @NonNull Messages.Result result) { validateBillingClient(); assert billingClient != null; AcknowledgePurchaseParams params = diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java index 7a66051b4e0..a9224e0211d 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java @@ -8,6 +8,7 @@ import androidx.annotation.Nullable; import com.android.billingclient.api.AccountIdentifiers; import com.android.billingclient.api.AlternativeBillingOnlyReportingDetails; +import com.android.billingclient.api.BillingClient; import com.android.billingclient.api.BillingConfig; import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.ProductDetails; @@ -20,7 +21,6 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; -import java.util.Map; /** * Handles serialization and deserialization of {@link com.android.billingclient.api.BillingClient} @@ -56,32 +56,41 @@ static HashMap fromProductDetail(ProductDetails detail) { return info; } - static List toProductList(List serialized) { + static List toProductList( + List platformProducts) { List products = new ArrayList<>(); - for (Object productSerialized : serialized) { - @SuppressWarnings(value = "unchecked") - Map productMap = (Map) productSerialized; - products.add(toProduct(productMap)); + for (Messages.PlatformProduct platformProduct : platformProducts) { + products.add(toProduct(platformProduct)); } return products; } - static QueryProductDetailsParams.Product toProduct(Map serialized) { - String productId = (String) serialized.get("productId"); - String productType = (String) serialized.get("productType"); + static QueryProductDetailsParams.Product toProduct(Messages.PlatformProduct platformProduct) { + return QueryProductDetailsParams.Product.newBuilder() - .setProductId(productId) - .setProductType(productType) + .setProductId(platformProduct.getProductId()) + .setProductType(toProductTypeString(platformProduct.getProductType())) .build(); } - static List> fromProductDetailsList( - @Nullable List productDetailsList) { + static String toProductTypeString(Messages.PlatformProductType type) { + switch (type) { + case INAPP: + return BillingClient.ProductType.INAPP; + case SUBS: + return BillingClient.ProductType.SUBS; + } + throw new Messages.FlutterError("UNKNOWN_TYPE", "Unknown product type: " + type, null); + } + + static List fromProductDetailsList(@Nullable List productDetailsList) { if (productDetailsList == null) { return Collections.emptyList(); } - ArrayList> output = new ArrayList<>(); + // This and the method are generically typed due to Pigeon limitations; see + // https://github.com/flutter/flutter/issues/116117. + ArrayList output = new ArrayList<>(); for (ProductDetails detail : productDetailsList) { output.add(fromProductDetail(detail)); } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index 64eafef5b19..30899da5ff6 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -8,7 +8,6 @@ import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.GET_BILLING_CONFIG; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.ON_DISCONNECT; -import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PRODUCT_DETAILS; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PURCHASES_ASYNC; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY; @@ -74,6 +73,9 @@ import io.flutter.plugins.inapppurchase.Messages.PlatformBillingChoiceMode; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingFlowParams; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingResult; +import io.flutter.plugins.inapppurchase.Messages.PlatformProduct; +import io.flutter.plugins.inapppurchase.Messages.PlatformProductDetailsResponse; +import io.flutter.plugins.inapppurchase.Messages.PlatformProductType; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; @@ -101,6 +103,7 @@ public class MethodCallHandlerTest { @Mock MethodChannel mockMethodChannel; @Spy Result result; @Spy Messages.Result platformBillingResult; + @Spy Messages.Result platformProductDetailsResult; @Mock Activity activity; @Mock Context context; @Mock ActivityPluginBinding mockActivityPluginBinding; @@ -432,14 +435,12 @@ public void endConnection() { public void queryProductDetailsAsync() { // Connect a billing client and set up the product query listeners establishConnectedBillingClient(); - String productType = BillingClient.ProductType.INAPP; - List productsList = asList("id1", "id2"); - HashMap arguments = new HashMap<>(); - arguments.put("productList", buildProductMap(productsList, productType)); - MethodCall queryCall = new MethodCall(QUERY_PRODUCT_DETAILS, arguments); + List productsIds = asList("id1", "id2"); + final List productList = + buildProductList(productsIds, PlatformProductType.INAPP); // Query for product details - methodChannelHandler.onMethodCall(queryCall, result); + methodChannelHandler.queryProductDetailsAsync(productList, platformProductDetailsResult); // Assert the arguments were forwarded correctly to BillingClient ArgumentCaptor paramCaptor = @@ -457,29 +458,32 @@ public void queryProductDetailsAsync() { .setDebugMessage("dummy debug message") .build(); listenerCaptor.getValue().onProductDetailsResponse(billingResult, productDetailsResponse); - verify(result).success(resultCaptor.capture()); - HashMap resultData = resultCaptor.getValue(); - assertEquals(resultData.get("billingResult"), fromBillingResult(billingResult)); + ArgumentCaptor resultCaptor = + ArgumentCaptor.forClass(PlatformProductDetailsResponse.class); + verify(platformProductDetailsResult).success(resultCaptor.capture()); + PlatformProductDetailsResponse resultData = resultCaptor.getValue(); + assertResultsMatch(resultData.getBillingResult(), billingResult); assertEquals( - resultData.get("productDetailsList"), fromProductDetailsList(productDetailsResponse)); + resultData.getProductDetailsJsonList(), fromProductDetailsList(productDetailsResponse)); } @Test public void queryProductDetailsAsync_clientDisconnected() { // Disconnect the Billing client and prepare a queryProductDetails call methodChannelHandler.endConnection(); - String productType = BillingClient.ProductType.INAPP; - List productsList = asList("id1", "id2"); - HashMap arguments = new HashMap<>(); - arguments.put("productList", buildProductMap(productsList, productType)); - MethodCall queryCall = new MethodCall(QUERY_PRODUCT_DETAILS, arguments); + List productsIds = asList("id1", "id2"); + final List productList = + buildProductList(productsIds, PlatformProductType.INAPP); - // Query for product details - methodChannelHandler.onMethodCall(queryCall, result); - - // Assert that we sent an error back. - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); - verify(result, never()).success(any()); + // Assert that we throw an error. + Messages.FlutterError exception = + assertThrows( + Messages.FlutterError.class, + () -> + methodChannelHandler.queryProductDetailsAsync( + productList, platformProductDetailsResult)); + assertEquals("UNAVAILABLE", exception.code); + assertTrue(Objects.requireNonNull(exception.getMessage()).contains("BillingClient")); } // Test launchBillingFlow not crash if `accountId` is `null` @@ -1071,14 +1075,12 @@ private void establishConnectedBillingClient() { private void queryForProducts(List productIdList) { // Set up the query method call establishConnectedBillingClient(); - HashMap arguments = new HashMap<>(); - String productType = BillingClient.ProductType.INAPP; - List> productList = buildProductMap(productIdList, productType); - arguments.put("productList", productList); - MethodCall queryCall = new MethodCall(QUERY_PRODUCT_DETAILS, arguments); + List productsIds = asList("id1", "id2"); + final List productList = + buildProductList(productsIds, PlatformProductType.INAPP); // Call the method. - methodChannelHandler.onMethodCall(queryCall, mock(Result.class)); + methodChannelHandler.queryProductDetailsAsync(productList, platformProductDetailsResult); // Respond to the call with a matching set of product details. ArgumentCaptor listenerCaptor = @@ -1095,13 +1097,13 @@ private void queryForProducts(List productIdList) { listenerCaptor.getValue().onProductDetailsResponse(billingResult, productDetailsResponse); } - private List> buildProductMap(List productIds, String productType) { - List> productList = new ArrayList<>(); + private List buildProductList( + List productIds, PlatformProductType productType) { + List productList = new ArrayList<>(); for (String productId : productIds) { - Map productMap = new HashMap<>(); - productMap.put("productId", productId); - productMap.put("productType", productType); - productList.add(productMap); + PlatformProduct.Builder builder = + new PlatformProduct.Builder().setProductId(productId).setProductType(productType); + productList.add(builder.build()); } return productList; } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java index aa32afe2e43..40c6a0b2d4d 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import org.json.JSONException; import org.junit.Before; import org.junit.Test; @@ -78,7 +79,7 @@ public void fromProductDetailsList() productDetailsConstructor.newInstance(IN_APP_PRODUCT_DETAIL_EXAMPLE_JSON), productDetailsConstructor.newInstance(SUBS_PRODUCT_DETAIL_EXAMPLE_JSON)); - final List> serialized = Translator.fromProductDetailsList(expected); + final List serialized = Translator.fromProductDetailsList(expected); assertEquals(expected.size(), serialized.size()); assertSerialized(expected.get(0), serialized.get(0)); @@ -191,8 +192,9 @@ public void currencyCodeFromSymbol() { } } - private void assertSerialized(ProductDetails expected, Map serialized) { - assertEquals(expected.getDescription(), serialized.get("description")); + private void assertSerialized(ProductDetails expected, Object serializedGeneric) { + @SuppressWarnings("unchecked") + final Map serialized = (Map) serializedGeneric; assertEquals(expected.getTitle(), serialized.get("title")); assertEquals(expected.getName(), serialized.get("name")); assertEquals(expected.getProductId(), serialized.get("productId")); @@ -286,7 +288,8 @@ private void assertSerialized(Purchase expected, Map serialized) assertEquals(expected.getDeveloperPayload(), serialized.get("developerPayload")); assertEquals(expected.isAcknowledged(), serialized.get("isAcknowledged")); assertEquals(expected.getPurchaseState(), serialized.get("purchaseState")); - assertNotNull(expected.getAccountIdentifiers().getObfuscatedAccountId()); + assertNotNull( + Objects.requireNonNull(expected.getAccountIdentifiers()).getObfuscatedAccountId()); assertEquals( expected.getAccountIdentifiers().getObfuscatedAccountId(), serialized.get("obfuscatedAccountId")); 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 2163e98f65f..02284967a9d 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 @@ -149,16 +149,11 @@ class BillingClient { Future queryProductDetails({ required List productList, }) async { - final Map arguments = { - 'productList': - productList.map((ProductWrapper product) => product.toJson()).toList() - }; - return ProductDetailsResponseWrapper.fromJson( - (await channel.invokeMapMethod( - 'BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener)', - arguments, - )) ?? - {}); + return productDetailsResponseWrapperFromPlatform( + await _hostApi.queryProductDetailsAsync(productList + .map( + (ProductWrapper product) => platformProductFromWrapper(product)) + .toList())); } /// Attempt to launch the Play Billing Flow for a given [productDetails]. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart index 5786bae49b5..48232f25a58 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -18,6 +18,12 @@ PlatformException _createConnectionError(String channelName) { ); } +/// Pigeon version of Java BillingClient.ProductType. +enum PlatformProductType { + inapp, + subs, +} + /// Pigeon version of billing_client_wrapper.dart's BillingChoiceMode. enum PlatformBillingChoiceMode { /// Billing through google play. @@ -29,6 +35,33 @@ enum PlatformBillingChoiceMode { alternativeBillingOnly, } +/// Pigeon version of Java Product. +class PlatformProduct { + PlatformProduct({ + required this.productId, + required this.productType, + }); + + String productId; + + PlatformProductType productType; + + Object encode() { + return [ + productId, + productType.index, + ]; + } + + static PlatformProduct decode(Object result) { + result as List; + return PlatformProduct( + productId: result[0]! as String, + productType: PlatformProductType.values[result[1]! as int], + ); + } +} + /// Pigeon version of Java BillingResult. class PlatformBillingResult { PlatformBillingResult({ @@ -56,6 +89,36 @@ class PlatformBillingResult { } } +/// Pigeon version of ProductDetailsResponseWrapper, which contains the +/// components of the java ProductDetailsResponseListener callback. +class PlatformProductDetailsResponse { + PlatformProductDetailsResponse({ + required this.billingResult, + required this.productDetailsJsonList, + }); + + PlatformBillingResult billingResult; + + /// A JSON-compatible list of details, where each entry in the list is a + /// Map JSON encoding of the product details. + List productDetailsJsonList; + + Object encode() { + return [ + billingResult.encode(), + productDetailsJsonList, + ]; + } + + static PlatformProductDetailsResponse decode(Object result) { + result as List; + return PlatformProductDetailsResponse( + billingResult: PlatformBillingResult.decode(result[0]! as List), + productDetailsJsonList: (result[1] as List?)!.cast(), + ); + } +} + /// Pigeon version of Java BillingFlowParams. class PlatformBillingFlowParams { PlatformBillingFlowParams({ @@ -118,6 +181,12 @@ class _InAppPurchaseApiCodec extends StandardMessageCodec { } else if (value is PlatformBillingResult) { buffer.putUint8(129); writeValue(buffer, value.encode()); + } else if (value is PlatformProduct) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is PlatformProductDetailsResponse) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -130,6 +199,10 @@ class _InAppPurchaseApiCodec extends StandardMessageCodec { return PlatformBillingFlowParams.decode(readValue(buffer)!); case 129: return PlatformBillingResult.decode(readValue(buffer)!); + case 130: + return PlatformProduct.decode(readValue(buffer)!); + case 131: + return PlatformProductDetailsResponse.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -325,6 +398,37 @@ class InAppPurchaseApi { } } + /// Wraps BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener). + Future queryProductDetailsAsync( + List products) async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.queryProductDetailsAsync'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send([products]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as PlatformProductDetailsResponse?)!; + } + } + /// Wraps BillingClient#isFeatureSupported(String). Future isFeatureSupported(String feature) async { const String __pigeon_channelName = diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart index 6c548ed19e3..a4bf5ec1c20 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart @@ -15,10 +15,38 @@ PlatformBillingChoiceMode platformBillingChoiceMode(BillingChoiceMode mode) { }; } -/// Converts a [BillingResultWrapper] to the Pigeon equivalent. +/// Creates a [BillingResultWrapper] from the Pigeon equivalent. BillingResultWrapper resultWrapperFromPlatform(PlatformBillingResult result) { return BillingResultWrapper( responseCode: const BillingResponseConverter().fromJson(result.responseCode), debugMessage: result.debugMessage); } + +/// Creates a [ProductDetailsResponseWrapper] from the Pigeon equivalent. +ProductDetailsResponseWrapper productDetailsResponseWrapperFromPlatform( + PlatformProductDetailsResponse response) { + return ProductDetailsResponseWrapper( + billingResult: resultWrapperFromPlatform(response.billingResult), + // See TODOs in messages.dart for why this is JSON. + productDetailsList: response.productDetailsJsonList + .map((Object? json) => ProductDetailsWrapper.fromJson( + (json! as Map).cast())) + .toList(), + ); +} + +/// Creates a Pigeon [PlatformProduct] from a [ProductWrapper]. +PlatformProduct platformProductFromWrapper(ProductWrapper product) { + return PlatformProduct( + productId: product.productId, + productType: _platformProductTypeFromWrapper(product.productType), + ); +} + +PlatformProductType _platformProductTypeFromWrapper(ProductType type) { + return switch (type) { + ProductType.inapp => PlatformProductType.inapp, + ProductType.subs => PlatformProductType.subs, + }; +} diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart index ff043eb1bfb..5db266ae6ba 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -12,6 +12,14 @@ import 'package:pigeon/pigeon.dart'; copyrightHeader: 'pigeons/copyright.txt', )) +/// Pigeon version of Java Product. +class PlatformProduct { + PlatformProduct({required this.productId, required this.productType}); + + final String productId; + final PlatformProductType productType; +} + /// Pigeon version of Java BillingResult. class PlatformBillingResult { PlatformBillingResult( @@ -20,6 +28,30 @@ class PlatformBillingResult { final String debugMessage; } +/// Pigeon version of ProductDetailsResponseWrapper, which contains the +/// components of the java ProductDetailsResponseListener callback. +class PlatformProductDetailsResponse { + PlatformProductDetailsResponse({ + required this.billingResult, + required this.productDetailsJsonList, + }); + + final PlatformBillingResult billingResult; + + /// A JSON-compatible list of details, where each entry in the list is a + /// Map JSON encoding of the product details. + // TODO(stuartmorgan): Finish converting to Pigeon. This is still using the + // old serialization system to allow conversion of all the method calls to + // Pigeon without converting the entire object graph all at once. See + // https://github.com/flutter/flutter/issues/117910. The list items are + // currently untyped due to https://github.com/flutter/flutter/issues/116117. + // + // TODO(stuartmorgan): Make the generic type non-nullable once supported. + // https://github.com/flutter/flutter/issues/97848 + // The consuming code treats all of it as non-nullable. + final List productDetailsJsonList; +} + /// Pigeon version of Java BillingFlowParams. class PlatformBillingFlowParams { PlatformBillingFlowParams({ @@ -44,6 +76,12 @@ class PlatformBillingFlowParams { final String? purchaseToken; } +/// Pigeon version of Java BillingClient.ProductType. +enum PlatformProductType { + inapp, + subs, +} + /// Pigeon version of billing_client_wrapper.dart's BillingChoiceMode. enum PlatformBillingChoiceMode { /// Billing through google play. @@ -79,6 +117,11 @@ abstract class InAppPurchaseApi { @async PlatformBillingResult consumeAsync(String purchaseToken); + /// Wraps BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener). + @async + PlatformProductDetailsResponse queryProductDetailsAsync( + List products); + /// Wraps BillingClient#isFeatureSupported(String). // TODO(stuartmorgan): Consider making this take a enum, and converting the // enum value to string constants on the native side, so that magic strings diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index eaab8ceef36..51cfcc18c4c 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -126,19 +126,17 @@ void main() { }); group('queryProductDetails', () { - const String queryMethodName = - 'BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener)'; - test('handles empty productDetails', () async { const String debugMessage = 'dummy message'; const BillingResponse responseCode = BillingResponse.developerError; - stubPlatform.addResponse(name: queryMethodName, value: { - 'billingResult': { - 'responseCode': const BillingResponseConverter().toJson(responseCode), - 'debugMessage': debugMessage, - }, - 'productDetailsList': >[] - }); + when(mockApi.queryProductDetailsAsync(any)) + .thenAnswer((_) async => PlatformProductDetailsResponse( + billingResult: PlatformBillingResult( + responseCode: + const BillingResponseConverter().toJson(responseCode), + debugMessage: debugMessage), + productDetailsJsonList: >[], + )); final ProductDetailsResponseWrapper response = await billingClient .queryProductDetails(productList: [ @@ -155,15 +153,16 @@ void main() { test('returns ProductDetailsResponseWrapper', () async { const String debugMessage = 'dummy message'; const BillingResponse responseCode = BillingResponse.ok; - stubPlatform.addResponse(name: queryMethodName, value: { - 'billingResult': { - 'responseCode': const BillingResponseConverter().toJson(responseCode), - 'debugMessage': debugMessage, - }, - 'productDetailsList': >[ - buildProductMap(dummyOneTimeProductDetails) - ], - }); + when(mockApi.queryProductDetailsAsync(any)) + .thenAnswer((_) async => PlatformProductDetailsResponse( + billingResult: PlatformBillingResult( + responseCode: + const BillingResponseConverter().toJson(responseCode), + debugMessage: debugMessage), + productDetailsJsonList: >[ + buildProductMap(dummyOneTimeProductDetails) + ], + )); final ProductDetailsResponseWrapper response = await billingClient.queryProductDetails( @@ -178,24 +177,6 @@ void main() { expect(response.billingResult, equals(billingResult)); expect(response.productDetailsList, contains(dummyOneTimeProductDetails)); }); - - test('handles null method channel response', () async { - stubPlatform.addResponse(name: queryMethodName); - - final ProductDetailsResponseWrapper response = - await billingClient.queryProductDetails( - productList: [ - const ProductWrapper( - productId: 'invalid', productType: ProductType.inapp), - ], - ); - - const BillingResultWrapper billingResult = BillingResultWrapper( - responseCode: BillingResponse.error, - debugMessage: kInvalidBillingResultErrorMessage); - expect(response.billingResult, equals(billingResult)); - expect(response.productDetailsList, isEmpty); - }); }); group('launchBillingFlow', () { diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart index 80b8a3ed3af..08d8f2c997d 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart @@ -32,6 +32,17 @@ class _FakePlatformBillingResult_0 extends _i1.SmartFake ); } +class _FakePlatformProductDetailsResponse_1 extends _i1.SmartFake + implements _i2.PlatformProductDetailsResponse { + _FakePlatformProductDetailsResponse_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [InAppPurchaseApi]. /// /// See the documentation for Mockito's code generation for more information. @@ -170,6 +181,33 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { )), ) as _i3.Future<_i2.PlatformBillingResult>); + @override + _i3.Future<_i2.PlatformProductDetailsResponse> queryProductDetailsAsync( + List<_i2.PlatformProduct?>? products) => + (super.noSuchMethod( + Invocation.method( + #queryProductDetailsAsync, + [products], + ), + returnValue: _i3.Future<_i2.PlatformProductDetailsResponse>.value( + _FakePlatformProductDetailsResponse_1( + this, + Invocation.method( + #queryProductDetailsAsync, + [products], + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.PlatformProductDetailsResponse>.value( + _FakePlatformProductDetailsResponse_1( + this, + Invocation.method( + #queryProductDetailsAsync, + [products], + ), + )), + ) as _i3.Future<_i2.PlatformProductDetailsResponse>); + @override _i3.Future isFeatureSupported(String? feature) => (super.noSuchMethod( Invocation.method( 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 ef5ebf9e07c..a8bd218cfe4 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 @@ -108,18 +108,17 @@ void main() { }); group('queryProductDetails', () { - const String queryMethodName = - 'BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener)'; - test('handles empty productDetails', () async { const String debugMessage = 'dummy message'; const BillingResponse responseCode = BillingResponse.ok; - const BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse(name: queryMethodName, value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'productDetailsList': >[], - }); + when(mockApi.queryProductDetailsAsync(any)) + .thenAnswer((_) async => PlatformProductDetailsResponse( + billingResult: PlatformBillingResult( + responseCode: + const BillingResponseConverter().toJson(responseCode), + debugMessage: debugMessage), + productDetailsJsonList: >[], + )); final ProductDetailsResponse response = await iapAndroidPlatform.queryProductDetails({''}); @@ -129,14 +128,16 @@ void main() { test('should get correct product details', () async { const String debugMessage = 'dummy message'; const BillingResponse responseCode = BillingResponse.ok; - const BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse(name: queryMethodName, value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'productDetailsList': >[ - buildProductMap(dummyOneTimeProductDetails) - ] - }); + when(mockApi.queryProductDetailsAsync(any)) + .thenAnswer((_) async => PlatformProductDetailsResponse( + billingResult: PlatformBillingResult( + responseCode: + const BillingResponseConverter().toJson(responseCode), + debugMessage: debugMessage), + productDetailsJsonList: >[ + buildProductMap(dummyOneTimeProductDetails) + ], + )); // Since queryProductDetails makes 2 platform method calls (one for each ProductType), the result will contain 2 dummyWrapper instead // of 1. final ProductDetailsResponse response = @@ -155,14 +156,16 @@ void main() { test('should get the correct notFoundIDs', () async { const String debugMessage = 'dummy message'; const BillingResponse responseCode = BillingResponse.ok; - const BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse(name: queryMethodName, value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'productDetailsList': >[ - buildProductMap(dummyOneTimeProductDetails) - ] - }); + when(mockApi.queryProductDetailsAsync(any)) + .thenAnswer((_) async => PlatformProductDetailsResponse( + billingResult: PlatformBillingResult( + responseCode: + const BillingResponseConverter().toJson(responseCode), + debugMessage: debugMessage), + productDetailsJsonList: >[ + buildProductMap(dummyOneTimeProductDetails) + ], + )); // Since queryProductDetails makes 2 platform method calls (one for each ProductType), the result will contain 2 dummyWrapper instead // of 1. final ProductDetailsResponse response = @@ -173,23 +176,13 @@ void main() { test( 'should have error stored in the response when platform exception is thrown', () async { - const BillingResponse responseCode = BillingResponse.ok; - stubPlatform.addResponse( - name: queryMethodName, - value: { - 'responseCode': - const BillingResponseConverter().toJson(responseCode), - 'productDetailsList': >[ - buildProductMap(dummyOneTimeProductDetails) - ] - }, - additionalStepBeforeReturn: (dynamic _) { - throw PlatformException( - code: 'error_code', - message: 'error_message', - details: {'info': 'error_info'}, - ); - }); + when(mockApi.queryProductDetailsAsync(any)).thenAnswer((_) async { + throw PlatformException( + code: 'error_code', + message: 'error_message', + details: {'info': 'error_info'}, + ); + }); // Since queryProductDetails makes 2 platform method calls (one for each ProductType), the result will contain 2 dummyWrapper instead // of 1. final ProductDetailsResponse response = From 8e51f8f250a23ae2d41645cbdf78a71cf2ae7c9b Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 6 Mar 2024 15:04:25 -0500 Subject: [PATCH 15/25] Convert createAlternativeBillingOnlyReportingDetailsAsync, and fix some incorrect error handling in previous conversions --- .../plugins/inapppurchase/Messages.java | 145 +++++++++++++++++- .../inapppurchase/MethodCallHandlerImpl.java | 91 ++++++----- .../plugins/inapppurchase/Translator.java | 25 ++- .../inapppurchase/MethodCallHandlerTest.java | 144 +++++++++-------- .../billing_client_wrapper.dart | 14 +- .../lib/src/messages.g.dart | 84 +++++++++- .../lib/src/pigeon_converters.dart | 11 ++ .../pigeons/messages.dart | 18 ++- .../billing_client_wrapper_test.dart | 37 ++--- .../billing_client_wrapper_test.mocks.dart | 43 ++++++ 10 files changed, 442 insertions(+), 170 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java index f1796e38368..f5842bfc5a7 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -256,7 +256,7 @@ ArrayList toList() { } /** - * Pigeon version of ProductDetailsResponseWrapper, which contains the components of the java + * Pigeon version of ProductDetailsResponseWrapper, which contains the components of the Java * ProductDetailsResponseListener callback. * *

Generated class from Pigeon that represents data sent in messages. @@ -342,6 +342,92 @@ ArrayList toList() { } } + /** + * Pigeon version of AlternativeBillingOnlyReportingDetailsWrapper, which contains the components + * of the Java AlternativeBillingOnlyReportingDetailsListener callback. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PlatformAlternativeBillingOnlyReportingDetailsResponse { + private @NonNull PlatformBillingResult billingResult; + + public @NonNull PlatformBillingResult getBillingResult() { + return billingResult; + } + + public void setBillingResult(@NonNull PlatformBillingResult setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"billingResult\" is null."); + } + this.billingResult = setterArg; + } + + private @NonNull String externalTransactionToken; + + public @NonNull String getExternalTransactionToken() { + return externalTransactionToken; + } + + public void setExternalTransactionToken(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"externalTransactionToken\" is null."); + } + this.externalTransactionToken = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PlatformAlternativeBillingOnlyReportingDetailsResponse() {} + + public static final class Builder { + + private @Nullable PlatformBillingResult billingResult; + + @CanIgnoreReturnValue + public @NonNull Builder setBillingResult(@NonNull PlatformBillingResult setterArg) { + this.billingResult = setterArg; + return this; + } + + private @Nullable String externalTransactionToken; + + @CanIgnoreReturnValue + public @NonNull Builder setExternalTransactionToken(@NonNull String setterArg) { + this.externalTransactionToken = setterArg; + return this; + } + + public @NonNull PlatformAlternativeBillingOnlyReportingDetailsResponse build() { + PlatformAlternativeBillingOnlyReportingDetailsResponse pigeonReturn = + new PlatformAlternativeBillingOnlyReportingDetailsResponse(); + pigeonReturn.setBillingResult(billingResult); + pigeonReturn.setExternalTransactionToken(externalTransactionToken); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add((billingResult == null) ? null : billingResult.toList()); + toListResult.add(externalTransactionToken); + return toListResult; + } + + static @NonNull PlatformAlternativeBillingOnlyReportingDetailsResponse fromList( + @NonNull ArrayList list) { + PlatformAlternativeBillingOnlyReportingDetailsResponse pigeonResult = + new PlatformAlternativeBillingOnlyReportingDetailsResponse(); + Object billingResult = list.get(0); + pigeonResult.setBillingResult( + (billingResult == null) + ? null + : PlatformBillingResult.fromList((ArrayList) billingResult)); + Object externalTransactionToken = list.get(1); + pigeonResult.setExternalTransactionToken((String) externalTransactionToken); + return pigeonResult; + } + } + /** * Pigeon version of Java BillingFlowParams. * @@ -570,12 +656,15 @@ private InAppPurchaseApiCodec() {} protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { case (byte) 128: - return PlatformBillingFlowParams.fromList((ArrayList) readValue(buffer)); + return PlatformAlternativeBillingOnlyReportingDetailsResponse.fromList( + (ArrayList) readValue(buffer)); case (byte) 129: - return PlatformBillingResult.fromList((ArrayList) readValue(buffer)); + return PlatformBillingFlowParams.fromList((ArrayList) readValue(buffer)); case (byte) 130: - return PlatformProduct.fromList((ArrayList) readValue(buffer)); + return PlatformBillingResult.fromList((ArrayList) readValue(buffer)); case (byte) 131: + return PlatformProduct.fromList((ArrayList) readValue(buffer)); + case (byte) 132: return PlatformProductDetailsResponse.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -584,17 +673,21 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { @Override protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { - if (value instanceof PlatformBillingFlowParams) { + if (value instanceof PlatformAlternativeBillingOnlyReportingDetailsResponse) { stream.write(128); + writeValue( + stream, ((PlatformAlternativeBillingOnlyReportingDetailsResponse) value).toList()); + } else if (value instanceof PlatformBillingFlowParams) { + stream.write(129); writeValue(stream, ((PlatformBillingFlowParams) value).toList()); } else if (value instanceof PlatformBillingResult) { - stream.write(129); + stream.write(130); writeValue(stream, ((PlatformBillingResult) value).toList()); } else if (value instanceof PlatformProduct) { - stream.write(130); + stream.write(131); writeValue(stream, ((PlatformProduct) value).toList()); } else if (value instanceof PlatformProductDetailsResponse) { - stream.write(131); + stream.write(132); writeValue(stream, ((PlatformProductDetailsResponse) value).toList()); } else { super.writeValue(stream, value); @@ -639,6 +732,12 @@ void queryProductDetailsAsync( void isAlternativeBillingOnlyAvailableAsync(@NonNull Result result); /** Wraps BillingClient#showAlternativeBillingOnlyInformationDialog(). */ void showAlternativeBillingOnlyInformationDialog(@NonNull Result result); + /** + * Wraps + * BillingClient#createAlternativeBillingOnlyReportingDetailsAsync(AlternativeBillingOnlyReportingDetailsListener). + */ + void createAlternativeBillingOnlyReportingDetailsAsync( + @NonNull Result result); /** The codec used by InAppPurchaseApi. */ static @NonNull MessageCodec getCodec() { @@ -931,6 +1030,36 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.createAlternativeBillingOnlyReportingDetailsAsync", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success( + PlatformAlternativeBillingOnlyReportingDetailsResponse result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.createAlternativeBillingOnlyReportingDetailsAsync(resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } } } } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index 5a34dd5f90e..cc40f853c6a 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -4,13 +4,13 @@ package io.flutter.plugins.inapppurchase; -import static io.flutter.plugins.inapppurchase.Translator.fromAlternativeBillingOnlyReportingDetails; import static io.flutter.plugins.inapppurchase.Translator.fromBillingConfig; import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult; import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; -import static io.flutter.plugins.inapppurchase.Translator.pigeonBillingResultFromBillingResult; +import static io.flutter.plugins.inapppurchase.Translator.pigeonResultFromAlternativeBillingOnlyReportingDetails; +import static io.flutter.plugins.inapppurchase.Translator.pigeonResultFromBillingResult; import static io.flutter.plugins.inapppurchase.Translator.toProductList; import android.app.Activity; @@ -61,8 +61,6 @@ static final class MethodNames { static final String QUERY_PURCHASE_HISTORY_ASYNC = "BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener)"; static final String GET_BILLING_CONFIG = "BillingClient#getBillingConfig()"; - static final String CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS = - "BillingClient#createAlternativeBillingOnlyReportingDetails()"; private MethodNames() {} } @@ -152,9 +150,6 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result case MethodNames.GET_BILLING_CONFIG: getBillingConfig(result); break; - case MethodNames.CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS: - createAlternativeBillingOnlyReportingDetails(result); - break; default: result.notImplemented(); } @@ -163,35 +158,42 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result @Override public void showAlternativeBillingOnlyInformationDialog( @NonNull Messages.Result result) { - validateBillingClient(); - assert billingClient != null; + if (billingClient == null) { + result.error(getNullBillingClientError()); + return; + } if (activity == null) { - throw new FlutterError(ACTIVITY_UNAVAILABLE, "Not attempting to show dialog", null); + result.error(new FlutterError(ACTIVITY_UNAVAILABLE, "Not attempting to show dialog", null)); + return; } billingClient.showAlternativeBillingOnlyInformationDialog( - activity, - billingResult -> result.success(pigeonBillingResultFromBillingResult(billingResult))); + activity, billingResult -> result.success(pigeonResultFromBillingResult(billingResult))); } - private void createAlternativeBillingOnlyReportingDetails(final MethodChannel.Result result) { - if (billingClientError(result)) { + @Override + public void createAlternativeBillingOnlyReportingDetailsAsync( + @NonNull + Messages.Result result) { + if (billingClient == null) { + result.error(getNullBillingClientError()); return; } - assert billingClient != null; billingClient.createAlternativeBillingOnlyReportingDetailsAsync( ((billingResult, alternativeBillingOnlyReportingDetails) -> result.success( - fromAlternativeBillingOnlyReportingDetails( + pigeonResultFromAlternativeBillingOnlyReportingDetails( billingResult, alternativeBillingOnlyReportingDetails)))); } @Override public void isAlternativeBillingOnlyAvailableAsync( @NonNull Messages.Result result) { - validateBillingClient(); - assert billingClient != null; + if (billingClient == null) { + result.error(getNullBillingClientError()); + return; + } billingClient.isAlternativeBillingOnlyAvailableAsync( - billingResult -> result.success(pigeonBillingResultFromBillingResult(billingResult))); + billingResult -> result.success(pigeonResultFromBillingResult(billingResult))); } private void getBillingConfig(final MethodChannel.Result result) { @@ -220,8 +222,9 @@ private void endBillingClientConnection() { @Override @NonNull public Boolean isReady() { - validateBillingClient(); - assert billingClient != null; + if (billingClient == null) { + throw getNullBillingClientError(); + } return billingClient.isReady(); } @@ -229,8 +232,10 @@ public Boolean isReady() { public void queryProductDetailsAsync( @NonNull List products, @NonNull Messages.Result result) { - validateBillingClient(); - assert billingClient != null; + if (billingClient == null) { + result.error(getNullBillingClientError()); + return; + } QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder().setProductList(toProductList(products)).build(); @@ -240,7 +245,7 @@ public void queryProductDetailsAsync( updateCachedProducts(productDetailsList); final PlatformProductDetailsResponse.Builder responseBuilder = new PlatformProductDetailsResponse.Builder() - .setBillingResult(pigeonBillingResultFromBillingResult(billingResult)) + .setBillingResult(pigeonResultFromBillingResult(billingResult)) .setProductDetailsJsonList(fromProductDetailsList(productDetailsList)); result.success(responseBuilder.build()); }); @@ -249,8 +254,9 @@ public void queryProductDetailsAsync( @Override public @NonNull PlatformBillingResult launchBillingFlow( @NonNull PlatformBillingFlowParams params) { - validateBillingClient(); - assert billingClient != null; + if (billingClient == null) { + throw getNullBillingClientError(); + } com.android.billingclient.api.ProductDetails productDetails = cachedProducts.get(params.getProduct()); @@ -345,7 +351,7 @@ public void queryProductDetailsAsync( subscriptionUpdateParamsBuilder, params.getProrationMode().intValue()); paramsBuilder.setSubscriptionUpdateParams(subscriptionUpdateParamsBuilder.build()); } - return pigeonBillingResultFromBillingResult( + return pigeonResultFromBillingResult( billingClient.launchBillingFlow(activity, paramsBuilder.build())); } @@ -362,12 +368,13 @@ private void setReplaceProrationMode( @Override public void consumeAsync( @NonNull String purchaseToken, @NonNull Messages.Result result) { - validateBillingClient(); - assert billingClient != null; + if (billingClient == null) { + result.error(getNullBillingClientError()); + return; + } ConsumeResponseListener listener = - (billingResult, outToken) -> - result.success(pigeonBillingResultFromBillingResult(billingResult)); + (billingResult, outToken) -> result.success(pigeonResultFromBillingResult(billingResult)); ConsumeParams.Builder paramsBuilder = ConsumeParams.newBuilder().setPurchaseToken(purchaseToken); ConsumeParams params = paramsBuilder.build(); @@ -437,7 +444,7 @@ public void onBillingSetupFinished(@NonNull BillingResult billingResult) { alreadyFinished = true; // Consider the fact that we've finished a success, leave it to the Dart side to // validate the responseCode. - result.success(pigeonBillingResultFromBillingResult(billingResult)); + result.success(pigeonResultFromBillingResult(billingResult)); } @Override @@ -452,13 +459,14 @@ public void onBillingServiceDisconnected() { @Override public void acknowledgePurchase( @NonNull String purchaseToken, @NonNull Messages.Result result) { - validateBillingClient(); - assert billingClient != null; + if (billingClient == null) { + result.error(getNullBillingClientError()); + return; + } AcknowledgePurchaseParams params = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchaseToken).build(); billingClient.acknowledgePurchase( - params, - billingResult -> result.success(pigeonBillingResultFromBillingResult(billingResult))); + params, billingResult -> result.success(pigeonResultFromBillingResult(billingResult))); } protected void updateCachedProducts(@Nullable List productDetailsList) { @@ -480,16 +488,15 @@ private boolean billingClientError(MethodChannel.Result result) { return true; } - private void validateBillingClient() { - if (billingClient == null) { - throw new FlutterError("UNAVAILABLE", "BillingClient is unset. Try reconnecting.", null); - } + private @NonNull FlutterError getNullBillingClientError() { + return new FlutterError("UNAVAILABLE", "BillingClient is unset. Try reconnecting.", null); } @Override public @NonNull Boolean isFeatureSupported(@NonNull String feature) { - validateBillingClient(); - assert billingClient != null; + if (billingClient == null) { + throw getNullBillingClientError(); + } BillingResult billingResult = billingClient.isFeatureSupported(feature); return billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK; } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java index a9224e0211d..d19724831df 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java @@ -242,12 +242,11 @@ static HashMap fromBillingResult(BillingResult billingResult) { return info; } - static Messages.PlatformBillingResult pigeonBillingResultFromBillingResult( - BillingResult billingResult) { - Messages.PlatformBillingResult.Builder builder = new Messages.PlatformBillingResult.Builder(); - builder.setResponseCode((long) billingResult.getResponseCode()); - builder.setDebugMessage(billingResult.getDebugMessage()); - return builder.build(); + static Messages.PlatformBillingResult pigeonResultFromBillingResult(BillingResult billingResult) { + return new Messages.PlatformBillingResult.Builder() + .setResponseCode((long) billingResult.getResponseCode()) + .setDebugMessage(billingResult.getDebugMessage()) + .build(); } /** Converter from {@link BillingResult} and {@link BillingConfig} to map. */ @@ -261,13 +260,13 @@ static HashMap fromBillingConfig( /** * Converter from {@link BillingResult} and {@link AlternativeBillingOnlyReportingDetails} to map. */ - static HashMap fromAlternativeBillingOnlyReportingDetails( - BillingResult result, AlternativeBillingOnlyReportingDetails details) { - HashMap info = fromBillingResult(result); - if (details != null) { - info.put("externalTransactionToken", details.getExternalTransactionToken()); - } - return info; + static Messages.PlatformAlternativeBillingOnlyReportingDetailsResponse + pigeonResultFromAlternativeBillingOnlyReportingDetails( + BillingResult result, AlternativeBillingOnlyReportingDetails details) { + return new Messages.PlatformAlternativeBillingOnlyReportingDetailsResponse.Builder() + .setBillingResult(pigeonResultFromBillingResult(result)) + .setExternalTransactionToken(details.getExternalTransactionToken()) + .build(); } /** diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index 30899da5ff6..8418c70e6ff 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -5,14 +5,12 @@ package io.flutter.plugins.inapppurchase; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.ACTIVITY_UNAVAILABLE; -import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.GET_BILLING_CONFIG; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.ON_DISCONNECT; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PURCHASES_ASYNC; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY; import static io.flutter.plugins.inapppurchase.PluginPurchaseListener.ON_PURCHASES_UPDATED; -import static io.flutter.plugins.inapppurchase.Translator.fromAlternativeBillingOnlyReportingDetails; import static io.flutter.plugins.inapppurchase.Translator.fromBillingConfig; import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult; import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList; @@ -70,6 +68,8 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugins.inapppurchase.Messages.FlutterError; +import io.flutter.plugins.inapppurchase.Messages.PlatformAlternativeBillingOnlyReportingDetailsResponse; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingChoiceMode; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingFlowParams; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingResult; @@ -104,6 +104,11 @@ public class MethodCallHandlerTest { @Spy Result result; @Spy Messages.Result platformBillingResult; @Spy Messages.Result platformProductDetailsResult; + + @Spy + Messages.Result + platformAlternativeBillingOnlyReportingDetailsResult; + @Mock Activity activity; @Mock Context context; @Mock ActivityPluginBinding mockActivityPluginBinding; @@ -155,8 +160,8 @@ public void isReady_false() { public void isReady_clientDisconnected() { methodChannelHandler.endConnection(); - Messages.FlutterError exception = - assertThrows(Messages.FlutterError.class, () -> methodChannelHandler.isReady()); + // Assert that the synchronous call throws an exception. + FlutterError exception = assertThrows(FlutterError.class, () -> methodChannelHandler.isReady()); assertEquals("UNAVAILABLE", exception.code); assertTrue(Objects.requireNonNull(exception.getMessage()).contains("BillingClient")); } @@ -285,8 +290,6 @@ public void createAlternativeBillingOnlyReportingDetailsSuccess() { mockStartConnection(); ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(AlternativeBillingOnlyReportingDetailsListener.class); - MethodCall createABOReportingDetailsCall = - new MethodCall(CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS, null); BillingResult billingResult = BillingResult.newBuilder() .setResponseCode(BillingResponseCode.OK) @@ -302,21 +305,33 @@ public void createAlternativeBillingOnlyReportingDetailsSuccess() { .when(mockBillingClient) .createAlternativeBillingOnlyReportingDetailsAsync(listenerCaptor.capture()); - methodChannelHandler.onMethodCall(createABOReportingDetailsCall, result); + methodChannelHandler.createAlternativeBillingOnlyReportingDetailsAsync( + platformAlternativeBillingOnlyReportingDetailsResult); listenerCaptor.getValue().onAlternativeBillingOnlyTokenResponse(billingResult, expectedDetails); - verify(result, times(1)) - .success(fromAlternativeBillingOnlyReportingDetails(billingResult, expectedDetails)); + verify(platformAlternativeBillingOnlyReportingDetailsResult, never()).error(any()); + ArgumentCaptor resultCaptor = + ArgumentCaptor.forClass(PlatformAlternativeBillingOnlyReportingDetailsResponse.class); + verify(platformAlternativeBillingOnlyReportingDetailsResult, times(1)) + .success(resultCaptor.capture()); + assertResultsMatch(resultCaptor.getValue().getBillingResult(), billingResult); + assertEquals( + resultCaptor.getValue().getExternalTransactionToken(), expectedExternalTransactionToken); } @Test public void createAlternativeBillingOnlyReportingDetails_serviceDisconnected() { - MethodCall createCall = new MethodCall(CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS, null); - methodChannelHandler.onMethodCall(createCall, mock(Result.class)); - - methodChannelHandler.onMethodCall(createCall, result); - - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); + methodChannelHandler.createAlternativeBillingOnlyReportingDetailsAsync( + platformAlternativeBillingOnlyReportingDetailsResult); + + // Assert that the async call returns an error result. + verify(platformAlternativeBillingOnlyReportingDetailsResult, never()).success(any()); + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); + verify(platformAlternativeBillingOnlyReportingDetailsResult, times(1)) + .error(errorCaptor.capture()); + assertEquals("UNAVAILABLE", errorCaptor.getValue().code); + assertTrue( + Objects.requireNonNull(errorCaptor.getValue().getMessage()).contains("BillingClient")); } @Test @@ -345,13 +360,15 @@ public void isAlternativeBillingOnlyAvailableSuccess() { @Test public void isAlternativeBillingOnlyAvailable_serviceDisconnected() { - Messages.FlutterError exception = - assertThrows( - Messages.FlutterError.class, - () -> - methodChannelHandler.isAlternativeBillingOnlyAvailableAsync(platformBillingResult)); - assertEquals("UNAVAILABLE", exception.code); - assertTrue(Objects.requireNonNull(exception.getMessage()).contains("BillingClient")); + methodChannelHandler.isAlternativeBillingOnlyAvailableAsync(platformBillingResult); + + // Assert that the async call returns an error result. + verify(platformBillingResult, never()).success(any()); + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); + verify(platformBillingResult, times(1)).error(errorCaptor.capture()); + assertEquals("UNAVAILABLE", errorCaptor.getValue().code); + assertTrue( + Objects.requireNonNull(errorCaptor.getValue().getMessage()).contains("BillingClient")); } @Test @@ -380,14 +397,15 @@ public void showAlternativeBillingOnlyInformationDialogSuccess() { @Test public void showAlternativeBillingOnlyInformationDialog_serviceDisconnected() { - Messages.FlutterError exception = - assertThrows( - Messages.FlutterError.class, - () -> - methodChannelHandler.showAlternativeBillingOnlyInformationDialog( - platformBillingResult)); - assertEquals("UNAVAILABLE", exception.code); - assertTrue(Objects.requireNonNull(exception.getMessage()).contains("BillingClient")); + methodChannelHandler.showAlternativeBillingOnlyInformationDialog(platformBillingResult); + + // Assert that the async call returns an error result. + verify(platformBillingResult, never()).success(any()); + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); + verify(platformBillingResult, times(1)).error(errorCaptor.capture()); + assertEquals("UNAVAILABLE", errorCaptor.getValue().code); + assertTrue( + Objects.requireNonNull(errorCaptor.getValue().getMessage()).contains("BillingClient")); } @Test @@ -395,15 +413,16 @@ public void showAlternativeBillingOnlyInformationDialog_NullActivity() { mockStartConnection(); methodChannelHandler.setActivity(null); - Messages.FlutterError exception = - assertThrows( - Messages.FlutterError.class, - () -> - methodChannelHandler.showAlternativeBillingOnlyInformationDialog( - platformBillingResult)); - assertEquals(ACTIVITY_UNAVAILABLE, exception.code); + methodChannelHandler.showAlternativeBillingOnlyInformationDialog(platformBillingResult); + + // Assert that the async call returns an error result. + verify(platformBillingResult, never()).success(any()); + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); + verify(platformBillingResult, times(1)).error(errorCaptor.capture()); + assertEquals(ACTIVITY_UNAVAILABLE, errorCaptor.getValue().code); assertTrue( - Objects.requireNonNull(exception.getMessage()).contains("Not attempting to show dialog")); + Objects.requireNonNull(errorCaptor.getValue().getMessage()) + .contains("Not attempting to show dialog")); } @Test @@ -475,15 +494,15 @@ public void queryProductDetailsAsync_clientDisconnected() { final List productList = buildProductList(productsIds, PlatformProductType.INAPP); - // Assert that we throw an error. - Messages.FlutterError exception = - assertThrows( - Messages.FlutterError.class, - () -> - methodChannelHandler.queryProductDetailsAsync( - productList, platformProductDetailsResult)); - assertEquals("UNAVAILABLE", exception.code); - assertTrue(Objects.requireNonNull(exception.getMessage()).contains("BillingClient")); + methodChannelHandler.queryProductDetailsAsync(productList, platformProductDetailsResult); + + // Assert that the async call returns an error result. + verify(platformProductDetailsResult, never()).success(any()); + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); + verify(platformProductDetailsResult, times(1)).error(errorCaptor.capture()); + assertEquals("UNAVAILABLE", errorCaptor.getValue().code); + assertTrue( + Objects.requireNonNull(errorCaptor.getValue().getMessage()).contains("BillingClient")); } // Test launchBillingFlow not crash if `accountId` is `null` @@ -561,14 +580,13 @@ public void launchBillingFlow_ok_null_Activity() { paramsBuilder.setProrationMode( (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); - // Verify the error response. - Messages.FlutterError exception = + // Assert that the synchronous call throws an exception. + FlutterError exception = assertThrows( - Messages.FlutterError.class, + FlutterError.class, () -> methodChannelHandler.launchBillingFlow(paramsBuilder.build())); assertEquals("ACTIVITY_UNAVAILABLE", exception.code); assertTrue(Objects.requireNonNull(exception.getMessage()).contains("foreground")); - verify(result, never()).success(any()); } @Test @@ -700,10 +718,10 @@ public void launchBillingFlow_ok_Proration_with_null_OldProduct() { .build(); when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); - // Assert that we sent an error back. - Messages.FlutterError exception = + // Assert that the synchronous call throws an exception. + FlutterError exception = assertThrows( - Messages.FlutterError.class, + FlutterError.class, () -> methodChannelHandler.launchBillingFlow(paramsBuilder.build())); assertEquals("IN_APP_PURCHASE_REQUIRE_OLD_PRODUCT", exception.code); assertTrue( @@ -762,10 +780,10 @@ public void launchBillingFlow_clientDisconnected() { paramsBuilder.setProrationMode( (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); - // Assert that we sent an error back. - Messages.FlutterError exception = + // Assert that the synchronous call throws an exception. + FlutterError exception = assertThrows( - Messages.FlutterError.class, + FlutterError.class, () -> methodChannelHandler.launchBillingFlow(paramsBuilder.build())); assertEquals("UNAVAILABLE", exception.code); assertTrue(Objects.requireNonNull(exception.getMessage()).contains("BillingClient")); @@ -783,10 +801,10 @@ public void launchBillingFlow_productNotFound() { paramsBuilder.setProrationMode( (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); - // Assert that we sent an error back. - Messages.FlutterError exception = + // Assert that the synchronous call throws an exception. + FlutterError exception = assertThrows( - Messages.FlutterError.class, + FlutterError.class, () -> methodChannelHandler.launchBillingFlow(paramsBuilder.build())); assertEquals("NOT_FOUND", exception.code); assertTrue(Objects.requireNonNull(exception.getMessage()).contains(productId)); @@ -807,10 +825,10 @@ public void launchBillingFlow_oldProductNotFound() { paramsBuilder.setProrationMode( (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); - // Assert that we sent an error back. - Messages.FlutterError exception = + // Assert that the synchronous call throws an exception. + FlutterError exception = assertThrows( - Messages.FlutterError.class, + FlutterError.class, () -> methodChannelHandler.launchBillingFlow(paramsBuilder.build())); assertEquals("IN_APP_PURCHASE_INVALID_OLD_PRODUCT", exception.code); assertTrue(Objects.requireNonNull(exception.getMessage()).contains(oldProductId)); 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 02284967a9d..5a384c89c92 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 @@ -333,22 +333,12 @@ class BillingClient { await _hostApi.showAlternativeBillingOnlyInformationDialog()); } - /// createAlternativeBillingOnlyReportingDetails method channel string identifier. - // - // Must match the value of CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS in - // ../../../android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java - @visibleForTesting - static const String createAlternativeBillingOnlyReportingDetailsMethodString = - 'BillingClient#createAlternativeBillingOnlyReportingDetails()'; - /// The details used to report transactions made via alternative billing /// without user choice to use Google Play billing. Future createAlternativeBillingOnlyReportingDetails() async { - return AlternativeBillingOnlyReportingDetailsWrapper.fromJson( - (await channel.invokeMapMethod( - createAlternativeBillingOnlyReportingDetailsMethodString)) ?? - {}); + return alternativeBillingOnlyReportingDetailsWrapperFromPlatform( + await _hostApi.createAlternativeBillingOnlyReportingDetailsAsync()); } /// The method call handler for [channel]. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart index 48232f25a58..bef915a4a14 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -90,7 +90,7 @@ class PlatformBillingResult { } /// Pigeon version of ProductDetailsResponseWrapper, which contains the -/// components of the java ProductDetailsResponseListener callback. +/// components of the Java ProductDetailsResponseListener callback. class PlatformProductDetailsResponse { PlatformProductDetailsResponse({ required this.billingResult, @@ -119,6 +119,36 @@ class PlatformProductDetailsResponse { } } +/// Pigeon version of AlternativeBillingOnlyReportingDetailsWrapper, which +/// contains the components of the Java +/// AlternativeBillingOnlyReportingDetailsListener callback. +class PlatformAlternativeBillingOnlyReportingDetailsResponse { + PlatformAlternativeBillingOnlyReportingDetailsResponse({ + required this.billingResult, + required this.externalTransactionToken, + }); + + PlatformBillingResult billingResult; + + String externalTransactionToken; + + Object encode() { + return [ + billingResult.encode(), + externalTransactionToken, + ]; + } + + static PlatformAlternativeBillingOnlyReportingDetailsResponse decode( + Object result) { + result as List; + return PlatformAlternativeBillingOnlyReportingDetailsResponse( + billingResult: PlatformBillingResult.decode(result[0]! as List), + externalTransactionToken: result[1]! as String, + ); + } +} + /// Pigeon version of Java BillingFlowParams. class PlatformBillingFlowParams { PlatformBillingFlowParams({ @@ -175,18 +205,21 @@ class _InAppPurchaseApiCodec extends StandardMessageCodec { const _InAppPurchaseApiCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { - if (value is PlatformBillingFlowParams) { + if (value is PlatformAlternativeBillingOnlyReportingDetailsResponse) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is PlatformBillingResult) { + } else if (value is PlatformBillingFlowParams) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is PlatformProduct) { + } else if (value is PlatformBillingResult) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is PlatformProductDetailsResponse) { + } else if (value is PlatformProduct) { buffer.putUint8(131); writeValue(buffer, value.encode()); + } else if (value is PlatformProductDetailsResponse) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -196,12 +229,15 @@ class _InAppPurchaseApiCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 128: - return PlatformBillingFlowParams.decode(readValue(buffer)!); + return PlatformAlternativeBillingOnlyReportingDetailsResponse.decode( + readValue(buffer)!); case 129: - return PlatformBillingResult.decode(readValue(buffer)!); + return PlatformBillingFlowParams.decode(readValue(buffer)!); case 130: - return PlatformProduct.decode(readValue(buffer)!); + return PlatformBillingResult.decode(readValue(buffer)!); case 131: + return PlatformProduct.decode(readValue(buffer)!); + case 132: return PlatformProductDetailsResponse.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -519,4 +555,36 @@ class InAppPurchaseApi { return (__pigeon_replyList[0] as PlatformBillingResult?)!; } } + + /// Wraps BillingClient#createAlternativeBillingOnlyReportingDetailsAsync(AlternativeBillingOnlyReportingDetailsListener). + Future + createAlternativeBillingOnlyReportingDetailsAsync() async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.createAlternativeBillingOnlyReportingDetailsAsync'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send(null) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] + as PlatformAlternativeBillingOnlyReportingDetailsResponse?)!; + } + } } diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart index a4bf5ec1c20..96237188470 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart @@ -36,6 +36,17 @@ ProductDetailsResponseWrapper productDetailsResponseWrapperFromPlatform( ); } +AlternativeBillingOnlyReportingDetailsWrapper + alternativeBillingOnlyReportingDetailsWrapperFromPlatform( + PlatformAlternativeBillingOnlyReportingDetailsResponse response) { + return AlternativeBillingOnlyReportingDetailsWrapper( + responseCode: const BillingResponseConverter() + .fromJson(response.billingResult.responseCode), + debugMessage: response.billingResult.debugMessage, + externalTransactionToken: response.externalTransactionToken, + ); +} + /// Creates a Pigeon [PlatformProduct] from a [ProductWrapper]. PlatformProduct platformProductFromWrapper(ProductWrapper product) { return PlatformProduct( diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart index 5db266ae6ba..b8b44fc8701 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -29,7 +29,7 @@ class PlatformBillingResult { } /// Pigeon version of ProductDetailsResponseWrapper, which contains the -/// components of the java ProductDetailsResponseListener callback. +/// components of the Java ProductDetailsResponseListener callback. class PlatformProductDetailsResponse { PlatformProductDetailsResponse({ required this.billingResult, @@ -52,6 +52,17 @@ class PlatformProductDetailsResponse { final List productDetailsJsonList; } +/// Pigeon version of AlternativeBillingOnlyReportingDetailsWrapper, which +/// contains the components of the Java +/// AlternativeBillingOnlyReportingDetailsListener callback. +class PlatformAlternativeBillingOnlyReportingDetailsResponse { + PlatformAlternativeBillingOnlyReportingDetailsResponse( + {required this.billingResult, required this.externalTransactionToken}); + + final PlatformBillingResult billingResult; + final String externalTransactionToken; +} + /// Pigeon version of Java BillingFlowParams. class PlatformBillingFlowParams { PlatformBillingFlowParams({ @@ -135,4 +146,9 @@ abstract class InAppPurchaseApi { /// Wraps BillingClient#showAlternativeBillingOnlyInformationDialog(). @async PlatformBillingResult showAlternativeBillingOnlyInformationDialog(); + + /// Wraps BillingClient#createAlternativeBillingOnlyReportingDetailsAsync(AlternativeBillingOnlyReportingDetailsListener). + @async + PlatformAlternativeBillingOnlyReportingDetailsResponse + createAlternativeBillingOnlyReportingDetailsAsync(); } diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index 51cfcc18c4c..c9e3be503f7 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -592,24 +592,14 @@ void main() { responseCode: BillingResponse.ok, debugMessage: 'debug', externalTransactionToken: 'abc123youandme'); - stubPlatform.addResponse( - name: BillingClient - .createAlternativeBillingOnlyReportingDetailsMethodString, - value: buildAlternativeBillingOnlyReportingDetailsMap(expected)); + when(mockApi.createAlternativeBillingOnlyReportingDetailsAsync()) + .thenAnswer((_) async => + platfromAlternativeBillingOnlyReportingDetailsFromWrapper( + expected)); final AlternativeBillingOnlyReportingDetailsWrapper result = await billingClient.createAlternativeBillingOnlyReportingDetails(); expect(result, equals(expected)); }); - - test('handles method channel returning null', () async { - stubPlatform.addResponse( - name: BillingClient - .createAlternativeBillingOnlyReportingDetailsMethodString, - ); - final AlternativeBillingOnlyReportingDetailsWrapper result = - await billingClient.createAlternativeBillingOnlyReportingDetails(); - expect(result.responseCode, BillingResponse.error); - }); }); group('showAlternativeBillingOnlyInformationDialog', () { @@ -635,13 +625,14 @@ Map buildBillingConfigMap(BillingConfigWrapper original) { }; } -Map buildAlternativeBillingOnlyReportingDetailsMap( - AlternativeBillingOnlyReportingDetailsWrapper original) { - return { - 'responseCode': - const BillingResponseConverter().toJson(original.responseCode), - 'debugMessage': original.debugMessage, - // from: io/flutter/plugins/inapppurchase/Translator.java - 'externalTransactionToken': original.externalTransactionToken, - }; +PlatformAlternativeBillingOnlyReportingDetailsResponse + platfromAlternativeBillingOnlyReportingDetailsFromWrapper( + AlternativeBillingOnlyReportingDetailsWrapper original) { + return PlatformAlternativeBillingOnlyReportingDetailsResponse( + billingResult: PlatformBillingResult( + responseCode: + const BillingResponseConverter().toJson(original.responseCode), + debugMessage: original.debugMessage!, + ), + externalTransactionToken: original.externalTransactionToken); } diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart index 08d8f2c997d..49e6678b5c7 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart @@ -43,6 +43,18 @@ class _FakePlatformProductDetailsResponse_1 extends _i1.SmartFake ); } +class _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_2 + extends _i1.SmartFake + implements _i2.PlatformAlternativeBillingOnlyReportingDetailsResponse { + _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [InAppPurchaseApi]. /// /// See the documentation for Mockito's code generation for more information. @@ -269,4 +281,35 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { ), )), ) as _i3.Future<_i2.PlatformBillingResult>); + + @override + _i3.Future<_i2.PlatformAlternativeBillingOnlyReportingDetailsResponse> + createAlternativeBillingOnlyReportingDetailsAsync() => + (super.noSuchMethod( + Invocation.method( + #createAlternativeBillingOnlyReportingDetailsAsync, + [], + ), + returnValue: _i3.Future< + _i2 + .PlatformAlternativeBillingOnlyReportingDetailsResponse>.value( + _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_2( + this, + Invocation.method( + #createAlternativeBillingOnlyReportingDetailsAsync, + [], + ), + )), + returnValueForMissingStub: _i3.Future< + _i2 + .PlatformAlternativeBillingOnlyReportingDetailsResponse>.value( + _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_2( + this, + Invocation.method( + #createAlternativeBillingOnlyReportingDetailsAsync, + [], + ), + )), + ) as _i3.Future< + _i2.PlatformAlternativeBillingOnlyReportingDetailsResponse>); } From 1c43bc96972c9f2b7266e27bb226b7aefbf166c8 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 6 Mar 2024 15:35:04 -0500 Subject: [PATCH 16/25] Convert getBillingConfigAsync, extract some Java test boilerplate --- .../plugins/inapppurchase/Messages.java | 136 +++++++++++++- .../inapppurchase/MethodCallHandlerImpl.java | 16 +- .../plugins/inapppurchase/Translator.java | 9 +- .../inapppurchase/MethodCallHandlerTest.java | 174 ++++++------------ .../billing_client_wrapper.dart | 13 +- .../lib/src/messages.g.dart | 77 +++++++- .../lib/src/pigeon_converters.dart | 13 ++ .../pigeons/messages.dart | 14 ++ .../billing_client_wrapper_test.dart | 40 ++-- .../billing_client_wrapper_test.mocks.dart | 53 +++++- ...rchase_android_platform_addition_test.dart | 6 +- 11 files changed, 352 insertions(+), 199 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java index f5842bfc5a7..522945732c1 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -428,6 +428,89 @@ ArrayList toList() { } } + /** + * Pigeon version of BillingConfigWrapper, which contains the components of the Java + * BillingConfigResponseListener callback. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PlatformBillingConfigResponse { + private @NonNull PlatformBillingResult billingResult; + + public @NonNull PlatformBillingResult getBillingResult() { + return billingResult; + } + + public void setBillingResult(@NonNull PlatformBillingResult setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"billingResult\" is null."); + } + this.billingResult = setterArg; + } + + private @NonNull String countryCode; + + public @NonNull String getCountryCode() { + return countryCode; + } + + public void setCountryCode(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"countryCode\" is null."); + } + this.countryCode = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PlatformBillingConfigResponse() {} + + public static final class Builder { + + private @Nullable PlatformBillingResult billingResult; + + @CanIgnoreReturnValue + public @NonNull Builder setBillingResult(@NonNull PlatformBillingResult setterArg) { + this.billingResult = setterArg; + return this; + } + + private @Nullable String countryCode; + + @CanIgnoreReturnValue + public @NonNull Builder setCountryCode(@NonNull String setterArg) { + this.countryCode = setterArg; + return this; + } + + public @NonNull PlatformBillingConfigResponse build() { + PlatformBillingConfigResponse pigeonReturn = new PlatformBillingConfigResponse(); + pigeonReturn.setBillingResult(billingResult); + pigeonReturn.setCountryCode(countryCode); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add((billingResult == null) ? null : billingResult.toList()); + toListResult.add(countryCode); + return toListResult; + } + + static @NonNull PlatformBillingConfigResponse fromList(@NonNull ArrayList list) { + PlatformBillingConfigResponse pigeonResult = new PlatformBillingConfigResponse(); + Object billingResult = list.get(0); + pigeonResult.setBillingResult( + (billingResult == null) + ? null + : PlatformBillingResult.fromList((ArrayList) billingResult)); + Object countryCode = list.get(1); + pigeonResult.setCountryCode((String) countryCode); + return pigeonResult; + } + } + /** * Pigeon version of Java BillingFlowParams. * @@ -659,12 +742,14 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { return PlatformAlternativeBillingOnlyReportingDetailsResponse.fromList( (ArrayList) readValue(buffer)); case (byte) 129: - return PlatformBillingFlowParams.fromList((ArrayList) readValue(buffer)); + return PlatformBillingConfigResponse.fromList((ArrayList) readValue(buffer)); case (byte) 130: - return PlatformBillingResult.fromList((ArrayList) readValue(buffer)); + return PlatformBillingFlowParams.fromList((ArrayList) readValue(buffer)); case (byte) 131: - return PlatformProduct.fromList((ArrayList) readValue(buffer)); + return PlatformBillingResult.fromList((ArrayList) readValue(buffer)); case (byte) 132: + return PlatformProduct.fromList((ArrayList) readValue(buffer)); + case (byte) 133: return PlatformProductDetailsResponse.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -677,17 +762,20 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { stream.write(128); writeValue( stream, ((PlatformAlternativeBillingOnlyReportingDetailsResponse) value).toList()); - } else if (value instanceof PlatformBillingFlowParams) { + } else if (value instanceof PlatformBillingConfigResponse) { stream.write(129); + writeValue(stream, ((PlatformBillingConfigResponse) value).toList()); + } else if (value instanceof PlatformBillingFlowParams) { + stream.write(130); writeValue(stream, ((PlatformBillingFlowParams) value).toList()); } else if (value instanceof PlatformBillingResult) { - stream.write(130); + stream.write(131); writeValue(stream, ((PlatformBillingResult) value).toList()); } else if (value instanceof PlatformProduct) { - stream.write(131); + stream.write(132); writeValue(stream, ((PlatformProduct) value).toList()); } else if (value instanceof PlatformProductDetailsResponse) { - stream.write(132); + stream.write(133); writeValue(stream, ((PlatformProductDetailsResponse) value).toList()); } else { super.writeValue(stream, value); @@ -707,6 +795,11 @@ void startConnection( @NonNull Result result); /** Wraps BillingClient#endConnection(BillingClientStateListener). */ void endConnection(); + /** + * Wraps BillingClient#getBillingConfigAsync(GetBillingConfigParams, + * BillingConfigResponseListener). + */ + void getBillingConfigAsync(@NonNull Result result); /** Wraps BillingClient#launchBillingFlow(Activity, BillingFlowParams). */ @NonNull PlatformBillingResult launchBillingFlow(@NonNull PlatformBillingFlowParams params); @@ -829,6 +922,35 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.getBillingConfigAsync", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success(PlatformBillingConfigResponse result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.getBillingConfigAsync(resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } { BasicMessageChannel channel = new BasicMessageChannel<>( diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index cc40f853c6a..39aefa0693d 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -4,12 +4,12 @@ package io.flutter.plugins.inapppurchase; -import static io.flutter.plugins.inapppurchase.Translator.fromBillingConfig; import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult; import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; import static io.flutter.plugins.inapppurchase.Translator.pigeonResultFromAlternativeBillingOnlyReportingDetails; +import static io.flutter.plugins.inapppurchase.Translator.pigeonResultFromBillingConfig; import static io.flutter.plugins.inapppurchase.Translator.pigeonResultFromBillingResult; import static io.flutter.plugins.inapppurchase.Translator.toProductList; @@ -60,7 +60,6 @@ static final class MethodNames { "BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)"; static final String QUERY_PURCHASE_HISTORY_ASYNC = "BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener)"; - static final String GET_BILLING_CONFIG = "BillingClient#getBillingConfig()"; private MethodNames() {} } @@ -147,9 +146,6 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result case MethodNames.QUERY_PURCHASE_HISTORY_ASYNC: queryPurchaseHistoryAsync((String) call.argument("productType"), result); break; - case MethodNames.GET_BILLING_CONFIG: - getBillingConfig(result); - break; default: result.notImplemented(); } @@ -196,15 +192,17 @@ public void isAlternativeBillingOnlyAvailableAsync( billingResult -> result.success(pigeonResultFromBillingResult(billingResult))); } - private void getBillingConfig(final MethodChannel.Result result) { - if (billingClientError(result)) { + @Override + public void getBillingConfigAsync( + @NonNull Messages.Result result) { + if (billingClient == null) { + result.error(getNullBillingClientError()); return; } - assert billingClient != null; billingClient.getBillingConfigAsync( GetBillingConfigParams.newBuilder().build(), (billingResult, billingConfig) -> - result.success(fromBillingConfig(billingResult, billingConfig))); + result.success(pigeonResultFromBillingConfig(billingResult, billingConfig))); } @Override diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java index d19724831df..5a75ed90c53 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java @@ -250,11 +250,12 @@ static Messages.PlatformBillingResult pigeonResultFromBillingResult(BillingResul } /** Converter from {@link BillingResult} and {@link BillingConfig} to map. */ - static HashMap fromBillingConfig( + static Messages.PlatformBillingConfigResponse pigeonResultFromBillingConfig( BillingResult result, BillingConfig billingConfig) { - HashMap info = fromBillingResult(result); - info.put("countryCode", billingConfig.getCountryCode()); - return info; + return new Messages.PlatformBillingConfigResponse.Builder() + .setBillingResult(pigeonResultFromBillingResult(result)) + .setCountryCode(billingConfig.getCountryCode()) + .build(); } /** diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index 8418c70e6ff..1784e2ac107 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -5,13 +5,11 @@ package io.flutter.plugins.inapppurchase; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.ACTIVITY_UNAVAILABLE; -import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.GET_BILLING_CONFIG; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.ON_DISCONNECT; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PURCHASES_ASYNC; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY; import static io.flutter.plugins.inapppurchase.PluginPurchaseListener.ON_PURCHASES_UPDATED; -import static io.flutter.plugins.inapppurchase.Translator.fromBillingConfig; import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult; import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList; @@ -71,6 +69,7 @@ import io.flutter.plugins.inapppurchase.Messages.FlutterError; import io.flutter.plugins.inapppurchase.Messages.PlatformAlternativeBillingOnlyReportingDetailsResponse; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingChoiceMode; +import io.flutter.plugins.inapppurchase.Messages.PlatformBillingConfigResponse; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingFlowParams; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingResult; import io.flutter.plugins.inapppurchase.Messages.PlatformProduct; @@ -102,13 +101,15 @@ public class MethodCallHandlerTest { @Mock BillingClient mockBillingClient; @Mock MethodChannel mockMethodChannel; @Spy Result result; - @Spy Messages.Result platformBillingResult; - @Spy Messages.Result platformProductDetailsResult; @Spy Messages.Result platformAlternativeBillingOnlyReportingDetailsResult; + @Spy Messages.Result platformBillingConfigResult; + @Spy Messages.Result platformBillingResult; + @Spy Messages.Result platformProductDetailsResult; + @Mock Activity activity; @Mock Context context; @Mock ActivityPluginBinding mockActivityPluginBinding; @@ -175,11 +176,7 @@ public void startConnection() { .createBillingClient( context, mockMethodChannel, PlatformBillingChoiceMode.PLAY_BILLING_ONLY); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); captor.getValue().onBillingSetupFinished(billingResult); ArgumentCaptor resultCaptor = @@ -197,11 +194,7 @@ public void startConnectionAlternativeBillingOnly() { .createBillingClient( context, mockMethodChannel, PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); captor.getValue().onBillingSetupFinished(billingResult); ArgumentCaptor resultCaptor = @@ -219,11 +212,7 @@ public void startConnection_multipleCalls() { methodChannelHandler.startConnection( 1L, PlatformBillingChoiceMode.PLAY_BILLING_ONLY, platformBillingResult); verify(platformBillingResult, never()).success(any()); - BillingResult billingResult1 = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult1 = buildBillingResult(); BillingResult billingResult2 = BillingResult.newBuilder() .setResponseCode(200) @@ -255,12 +244,7 @@ public void getBillingConfigSuccess() { ArgumentCaptor.forClass(GetBillingConfigParams.class); ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(BillingConfigResponseListener.class); - MethodCall billingCall = new MethodCall(GET_BILLING_CONFIG, null); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); final String expectedCountryCode = "US"; final BillingConfig expectedConfig = mock(BillingConfig.class); when(expectedConfig.getCountryCode()).thenReturn(expectedCountryCode); @@ -269,20 +253,27 @@ public void getBillingConfigSuccess() { .when(mockBillingClient) .getBillingConfigAsync(paramsCaptor.capture(), listenerCaptor.capture()); - methodChannelHandler.onMethodCall(billingCall, result); + methodChannelHandler.getBillingConfigAsync(platformBillingConfigResult); listenerCaptor.getValue().onBillingConfigResponse(billingResult, expectedConfig); - verify(result, times(1)).success(fromBillingConfig(billingResult, expectedConfig)); + ArgumentCaptor resultCaptor = + ArgumentCaptor.forClass(PlatformBillingConfigResponse.class); + verify(platformBillingConfigResult, times(1)).success(resultCaptor.capture()); + assertResultsMatch(resultCaptor.getValue().getBillingResult(), billingResult); + assertEquals(resultCaptor.getValue().getCountryCode(), expectedCountryCode); } @Test public void getBillingConfig_serviceDisconnected() { - MethodCall billingCall = new MethodCall(GET_BILLING_CONFIG, null); - methodChannelHandler.onMethodCall(billingCall, mock(Result.class)); - - methodChannelHandler.onMethodCall(billingCall, result); + methodChannelHandler.getBillingConfigAsync(platformBillingConfigResult); - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); + // Assert that the async call returns an error result. + verify(platformBillingConfigResult, never()).success(any()); + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); + verify(platformBillingConfigResult, times(1)).error(errorCaptor.capture()); + assertEquals("UNAVAILABLE", errorCaptor.getValue().code); + assertTrue( + Objects.requireNonNull(errorCaptor.getValue().getMessage()).contains("BillingClient")); } @Test @@ -290,11 +281,7 @@ public void createAlternativeBillingOnlyReportingDetailsSuccess() { mockStartConnection(); ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(AlternativeBillingOnlyReportingDetailsListener.class); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(BillingResponseCode.OK) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(BillingResponseCode.OK); final AlternativeBillingOnlyReportingDetails expectedDetails = mock(AlternativeBillingOnlyReportingDetails.class); final String expectedExternalTransactionToken = "abc123youandme"; @@ -339,11 +326,7 @@ public void isAlternativeBillingOnlyAvailableSuccess() { mockStartConnection(); ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(AlternativeBillingOnlyAvailabilityListener.class); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(BillingClient.BillingResponseCode.OK) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(BillingClient.BillingResponseCode.OK); doNothing() .when(mockBillingClient) @@ -376,11 +359,7 @@ public void showAlternativeBillingOnlyInformationDialogSuccess() { mockStartConnection(); ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(AlternativeBillingOnlyInformationDialogListener.class); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(BillingResponseCode.OK) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(BillingClient.BillingResponseCode.OK); when(mockBillingClient.showAlternativeBillingOnlyInformationDialog( eq(activity), listenerCaptor.capture())) @@ -471,11 +450,7 @@ public void queryProductDetailsAsync() { // Assert that we handed result BillingClient's response List productDetailsResponse = singletonList(buildProductDetails("foo")); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); listenerCaptor.getValue().onProductDetailsResponse(billingResult, productDetailsResponse); ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(PlatformProductDetailsResponse.class); @@ -519,11 +494,7 @@ public void launchBillingFlow_null_AccountId_do_not_crash() { (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); // Launch the billing flow - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); PlatformBillingResult platformResult = methodChannelHandler.launchBillingFlow(paramsBuilder.build()); @@ -548,11 +519,7 @@ public void launchBillingFlow_ok_null_OldProduct() { (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); // Launch the billing flow - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); PlatformBillingResult platformResult = methodChannelHandler.launchBillingFlow(paramsBuilder.build()); @@ -604,11 +571,7 @@ public void launchBillingFlow_ok_oldProduct() { (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); // Launch the billing flow - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); PlatformBillingResult platformResult = methodChannelHandler.launchBillingFlow(paramsBuilder.build()); @@ -635,11 +598,7 @@ public void launchBillingFlow_ok_AccountId() { (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); // Launch the billing flow - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); PlatformBillingResult platformResult = methodChannelHandler.launchBillingFlow(paramsBuilder.build()); @@ -674,11 +633,7 @@ public void launchBillingFlow_ok_Proration() { paramsBuilder.setProrationMode((long) prorationMode); // Launch the billing flow - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); PlatformBillingResult platformResult = methodChannelHandler.launchBillingFlow(paramsBuilder.build()); @@ -711,11 +666,7 @@ public void launchBillingFlow_ok_Proration_with_null_OldProduct() { paramsBuilder.setProrationMode((long) prorationMode); // Launch the billing flow - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); // Assert that the synchronous call throws an exception. @@ -750,11 +701,7 @@ public void launchBillingFlow_ok_Full() { paramsBuilder.setProrationMode((long) prorationMode); // Launch the billing flow - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); PlatformBillingResult platformResult = methodChannelHandler.launchBillingFlow(paramsBuilder.build()); @@ -902,11 +849,7 @@ public void queryPurchases_returns_success() throws Exception { public void queryPurchaseHistoryAsync() { // Set up an established billing client and all our mocked responses establishConnectedBillingClient(); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); List purchasesList = singletonList(buildPurchaseHistoryRecord("foo")); HashMap arguments = new HashMap<>(); arguments.put("productType", BillingClient.ProductType.INAPP); @@ -945,11 +888,7 @@ public void queryPurchaseHistoryAsync_clientDisconnected() { public void onPurchasesUpdatedListener() { PluginPurchaseListener listener = new PluginPurchaseListener(mockMethodChannel); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); List purchasesList = singletonList(buildPurchase("foo")); doNothing() .when(mockMethodChannel) @@ -964,11 +903,7 @@ public void onPurchasesUpdatedListener() { @Test public void consumeAsync() { establishConnectedBillingClient(); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); final String token = "mockToken"; ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(ConsumeResponseListener.class); @@ -993,11 +928,7 @@ public void consumeAsync() { @Test public void acknowledgePurchase() { establishConnectedBillingClient(); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); final String purchaseToken = "mockToken"; ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(AcknowledgePurchaseResponseListener.class); @@ -1034,11 +965,7 @@ public void isFutureSupported_true() { mockStartConnection(); final String feature = "subscriptions"; - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(BillingClient.BillingResponseCode.OK) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(BillingClient.BillingResponseCode.OK); when(mockBillingClient.isFeatureSupported(feature)).thenReturn(billingResult); assertTrue(methodChannelHandler.isFeatureSupported(feature)); @@ -1049,11 +976,7 @@ public void isFutureSupported_false() { mockStartConnection(); final String feature = "subscriptions"; - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(BillingResponseCode.FEATURE_NOT_SUPPORTED); when(mockBillingClient.isFeatureSupported(feature)).thenReturn(billingResult); assertFalse(methodChannelHandler.isFeatureSupported(feature)); @@ -1107,11 +1030,7 @@ private void queryForProducts(List productIdList) { List productDetailsResponse = productIdList.stream().map(this::buildProductDetails).collect(toList()); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); listenerCaptor.getValue().onProductDetailsResponse(billingResult, productDetailsResponse); } @@ -1161,6 +1080,17 @@ private PurchaseHistoryRecord buildPurchaseHistoryRecord(String purchaseToken) { return purchase; } + private BillingResult buildBillingResult() { + return buildBillingResult(100); + } + + private BillingResult buildBillingResult(int responseCode) { + return BillingResult.newBuilder() + .setResponseCode(responseCode) + .setDebugMessage("dummy debug message") + .build(); + } + private void assertResultsMatch(PlatformBillingResult pigeonResult, BillingResult nativeResult) { assertEquals(pigeonResult.getResponseCode().longValue(), nativeResult.getResponseCode()); assertEquals(pigeonResult.getDebugMessage(), nativeResult.getDebugMessage()); 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 5a384c89c92..5b7e98159eb 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 @@ -305,19 +305,10 @@ class BillingClient { const BillingClientFeatureConverter().toJson(feature)); } - /// BillingConfig method channel string identifier. - // - // Must match the value of GET_BILLING_CONFIG in - // ../../../android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java - @visibleForTesting - static const String getBillingConfigMethodString = - 'BillingClient#getBillingConfig()'; - /// Fetches billing config info into a [BillingConfigWrapper] object. Future getBillingConfig() async { - return BillingConfigWrapper.fromJson((await channel - .invokeMapMethod(getBillingConfigMethodString)) ?? - {}); + return billingConfigWrapperFromPlatform( + await _hostApi.getBillingConfigAsync()); } /// Checks if "AlterntitiveBillingOnly" feature is available. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart index bef915a4a14..bcdb0a5e0e3 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -149,6 +149,34 @@ class PlatformAlternativeBillingOnlyReportingDetailsResponse { } } +/// Pigeon version of BillingConfigWrapper, which contains the components of the +/// Java BillingConfigResponseListener callback. +class PlatformBillingConfigResponse { + PlatformBillingConfigResponse({ + required this.billingResult, + required this.countryCode, + }); + + PlatformBillingResult billingResult; + + String countryCode; + + Object encode() { + return [ + billingResult.encode(), + countryCode, + ]; + } + + static PlatformBillingConfigResponse decode(Object result) { + result as List; + return PlatformBillingConfigResponse( + billingResult: PlatformBillingResult.decode(result[0]! as List), + countryCode: result[1]! as String, + ); + } +} + /// Pigeon version of Java BillingFlowParams. class PlatformBillingFlowParams { PlatformBillingFlowParams({ @@ -208,18 +236,21 @@ class _InAppPurchaseApiCodec extends StandardMessageCodec { if (value is PlatformAlternativeBillingOnlyReportingDetailsResponse) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is PlatformBillingFlowParams) { + } else if (value is PlatformBillingConfigResponse) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is PlatformBillingResult) { + } else if (value is PlatformBillingFlowParams) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is PlatformProduct) { + } else if (value is PlatformBillingResult) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is PlatformProductDetailsResponse) { + } else if (value is PlatformProduct) { buffer.putUint8(132); writeValue(buffer, value.encode()); + } else if (value is PlatformProductDetailsResponse) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -232,12 +263,14 @@ class _InAppPurchaseApiCodec extends StandardMessageCodec { return PlatformAlternativeBillingOnlyReportingDetailsResponse.decode( readValue(buffer)!); case 129: - return PlatformBillingFlowParams.decode(readValue(buffer)!); + return PlatformBillingConfigResponse.decode(readValue(buffer)!); case 130: - return PlatformBillingResult.decode(readValue(buffer)!); + return PlatformBillingFlowParams.decode(readValue(buffer)!); case 131: - return PlatformProduct.decode(readValue(buffer)!); + return PlatformBillingResult.decode(readValue(buffer)!); case 132: + return PlatformProduct.decode(readValue(buffer)!); + case 133: return PlatformProductDetailsResponse.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -342,6 +375,36 @@ class InAppPurchaseApi { } } + /// Wraps BillingClient#getBillingConfigAsync(GetBillingConfigParams, BillingConfigResponseListener). + Future getBillingConfigAsync() async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.getBillingConfigAsync'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send(null) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as PlatformBillingConfigResponse?)!; + } + } + /// Wraps BillingClient#launchBillingFlow(Activity, BillingFlowParams). Future launchBillingFlow( PlatformBillingFlowParams params) async { diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart index 96237188470..6f5719cd3a2 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import '../billing_client_wrappers.dart'; +import 'billing_client_wrappers/billing_config_wrapper.dart'; import 'messages.g.dart'; /// Converts a [BillingChoiceMode] to the Pigeon equivalent. @@ -36,6 +37,8 @@ ProductDetailsResponseWrapper productDetailsResponseWrapperFromPlatform( ); } +/// Creates an [AlternativeBillingOnlyReportingDetailsWrapper] from the Pigeon +/// equivalent. AlternativeBillingOnlyReportingDetailsWrapper alternativeBillingOnlyReportingDetailsWrapperFromPlatform( PlatformAlternativeBillingOnlyReportingDetailsResponse response) { @@ -47,6 +50,16 @@ AlternativeBillingOnlyReportingDetailsWrapper ); } +BillingConfigWrapper billingConfigWrapperFromPlatform( + PlatformBillingConfigResponse response) { + return BillingConfigWrapper( + responseCode: const BillingResponseConverter() + .fromJson(response.billingResult.responseCode), + debugMessage: response.billingResult.debugMessage, + countryCode: response.countryCode, + ); +} + /// Creates a Pigeon [PlatformProduct] from a [ProductWrapper]. PlatformProduct platformProductFromWrapper(ProductWrapper product) { return PlatformProduct( diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart index b8b44fc8701..e1f1344520a 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -63,6 +63,16 @@ class PlatformAlternativeBillingOnlyReportingDetailsResponse { final String externalTransactionToken; } +/// Pigeon version of BillingConfigWrapper, which contains the components of the +/// Java BillingConfigResponseListener callback. +class PlatformBillingConfigResponse { + PlatformBillingConfigResponse( + {required this.billingResult, required this.countryCode}); + + final PlatformBillingResult billingResult; + final String countryCode; +} + /// Pigeon version of Java BillingFlowParams. class PlatformBillingFlowParams { PlatformBillingFlowParams({ @@ -117,6 +127,10 @@ abstract class InAppPurchaseApi { /// Wraps BillingClient#endConnection(BillingClientStateListener). void endConnection(); + /// Wraps BillingClient#getBillingConfigAsync(GetBillingConfigParams, BillingConfigResponseListener). + @async + PlatformBillingConfigResponse getBillingConfigAsync(); + /// Wraps BillingClient#launchBillingFlow(Activity, BillingFlowParams). PlatformBillingResult launchBillingFlow(PlatformBillingFlowParams params); diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index c9e3be503f7..ce7ca1b039c 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -547,29 +547,13 @@ void main() { countryCode: 'US', responseCode: BillingResponse.ok, debugMessage: ''); - stubPlatform.addResponse( - name: BillingClient.getBillingConfigMethodString, - value: buildBillingConfigMap(expected), - ); + when(mockApi.getBillingConfigAsync()) + .thenAnswer((_) async => platformBillingConfigFromWrapper(expected)); final BillingConfigWrapper result = await billingClient.getBillingConfig(); expect(result.countryCode, 'US'); expect(result, expected); }); - - test('handles method channel returning null', () async { - stubPlatform.addResponse( - name: BillingClient.getBillingConfigMethodString, - ); - final BillingConfigWrapper result = - await billingClient.getBillingConfig(); - expect( - result, - equals(const BillingConfigWrapper( - responseCode: BillingResponse.error, - debugMessage: kInvalidBillingConfigErrorMessage, - ))); - }); }); group('isAlternativeBillingOnlyAvailable', () { @@ -594,7 +578,7 @@ void main() { externalTransactionToken: 'abc123youandme'); when(mockApi.createAlternativeBillingOnlyReportingDetailsAsync()) .thenAnswer((_) async => - platfromAlternativeBillingOnlyReportingDetailsFromWrapper( + platformAlternativeBillingOnlyReportingDetailsFromWrapper( expected)); final AlternativeBillingOnlyReportingDetailsWrapper result = await billingClient.createAlternativeBillingOnlyReportingDetails(); @@ -616,17 +600,19 @@ void main() { }); } -Map buildBillingConfigMap(BillingConfigWrapper original) { - return { - 'responseCode': - const BillingResponseConverter().toJson(original.responseCode), - 'debugMessage': original.debugMessage, - 'countryCode': original.countryCode, - }; +PlatformBillingConfigResponse platformBillingConfigFromWrapper( + BillingConfigWrapper original) { + return PlatformBillingConfigResponse( + billingResult: PlatformBillingResult( + responseCode: + const BillingResponseConverter().toJson(original.responseCode), + debugMessage: original.debugMessage!, + ), + countryCode: original.countryCode); } PlatformAlternativeBillingOnlyReportingDetailsResponse - platfromAlternativeBillingOnlyReportingDetailsFromWrapper( + platformAlternativeBillingOnlyReportingDetailsFromWrapper( AlternativeBillingOnlyReportingDetailsWrapper original) { return PlatformAlternativeBillingOnlyReportingDetailsResponse( billingResult: PlatformBillingResult( diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart index 49e6678b5c7..a16f93f920b 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart @@ -32,9 +32,20 @@ class _FakePlatformBillingResult_0 extends _i1.SmartFake ); } -class _FakePlatformProductDetailsResponse_1 extends _i1.SmartFake +class _FakePlatformBillingConfigResponse_1 extends _i1.SmartFake + implements _i2.PlatformBillingConfigResponse { + _FakePlatformBillingConfigResponse_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformProductDetailsResponse_2 extends _i1.SmartFake implements _i2.PlatformProductDetailsResponse { - _FakePlatformProductDetailsResponse_1( + _FakePlatformProductDetailsResponse_2( Object parent, Invocation parentInvocation, ) : super( @@ -43,10 +54,10 @@ class _FakePlatformProductDetailsResponse_1 extends _i1.SmartFake ); } -class _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_2 +class _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_3 extends _i1.SmartFake implements _i2.PlatformAlternativeBillingOnlyReportingDetailsResponse { - _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_2( + _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_3( Object parent, Invocation parentInvocation, ) : super( @@ -116,6 +127,32 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { returnValueForMissingStub: _i3.Future.value(), ) as _i3.Future); + @override + _i3.Future<_i2.PlatformBillingConfigResponse> getBillingConfigAsync() => + (super.noSuchMethod( + Invocation.method( + #getBillingConfigAsync, + [], + ), + returnValue: _i3.Future<_i2.PlatformBillingConfigResponse>.value( + _FakePlatformBillingConfigResponse_1( + this, + Invocation.method( + #getBillingConfigAsync, + [], + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.PlatformBillingConfigResponse>.value( + _FakePlatformBillingConfigResponse_1( + this, + Invocation.method( + #getBillingConfigAsync, + [], + ), + )), + ) as _i3.Future<_i2.PlatformBillingConfigResponse>); + @override _i3.Future<_i2.PlatformBillingResult> launchBillingFlow( _i2.PlatformBillingFlowParams? params) => @@ -202,7 +239,7 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { [products], ), returnValue: _i3.Future<_i2.PlatformProductDetailsResponse>.value( - _FakePlatformProductDetailsResponse_1( + _FakePlatformProductDetailsResponse_2( this, Invocation.method( #queryProductDetailsAsync, @@ -211,7 +248,7 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { )), returnValueForMissingStub: _i3.Future<_i2.PlatformProductDetailsResponse>.value( - _FakePlatformProductDetailsResponse_1( + _FakePlatformProductDetailsResponse_2( this, Invocation.method( #queryProductDetailsAsync, @@ -293,7 +330,7 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { returnValue: _i3.Future< _i2 .PlatformAlternativeBillingOnlyReportingDetailsResponse>.value( - _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_2( + _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_3( this, Invocation.method( #createAlternativeBillingOnlyReportingDetailsAsync, @@ -303,7 +340,7 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { returnValueForMissingStub: _i3.Future< _i2 .PlatformAlternativeBillingOnlyReportingDetailsResponse>.value( - _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_2( + _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_3( this, Invocation.method( #createAlternativeBillingOnlyReportingDetailsAsync, 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 4cb595098c2..7a1c9ab481f 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 @@ -68,10 +68,8 @@ void main() { responseCode: BillingResponse.ok, debugMessage: 'dummy message'); - stubPlatform.addResponse( - name: BillingClient.getBillingConfigMethodString, - value: buildBillingConfigMap(expected), - ); + when(mockApi.getBillingConfigAsync()) + .thenAnswer((_) async => platformBillingConfigFromWrapper(expected)); final String countryCode = await iapAndroidPlatformAddition.getCountryCode(); From 8a6803ea8d2254893143cab149c975b1050007c9 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 6 Mar 2024 15:39:47 -0500 Subject: [PATCH 17/25] Ensure that success and error are mutually exlusive everywhere --- .../plugins/inapppurchase/MethodCallHandlerTest.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index 1784e2ac107..095c9ca3880 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -183,6 +183,7 @@ public void startConnection() { ArgumentCaptor.forClass(PlatformBillingResult.class); verify(platformBillingResult, times(1)).success(resultCaptor.capture()); assertResultsMatch(resultCaptor.getValue(), billingResult); + verify(platformBillingResult, never()).error(any()); } @Test @@ -201,6 +202,7 @@ public void startConnectionAlternativeBillingOnly() { ArgumentCaptor.forClass(PlatformBillingResult.class); verify(platformBillingResult, times(1)).success(resultCaptor.capture()); assertResultsMatch(resultCaptor.getValue(), billingResult); + verify(platformBillingResult, never()).error(any()); } @Test @@ -234,7 +236,7 @@ public void startConnection_multipleCalls() { assertEquals( resultCaptor.getValue().getResponseCode().longValue(), billingResult1.getResponseCode()); assertEquals(resultCaptor.getValue().getDebugMessage(), billingResult1.getDebugMessage()); - verify(platformBillingResult, times(1)).success(any()); + verify(platformBillingResult, never()).error(any()); } @Test @@ -261,6 +263,7 @@ public void getBillingConfigSuccess() { verify(platformBillingConfigResult, times(1)).success(resultCaptor.capture()); assertResultsMatch(resultCaptor.getValue().getBillingResult(), billingResult); assertEquals(resultCaptor.getValue().getCountryCode(), expectedCountryCode); + verify(platformBillingConfigResult, never()).error(any()); } @Test @@ -339,6 +342,7 @@ public void isAlternativeBillingOnlyAvailableSuccess() { ArgumentCaptor.forClass(PlatformBillingResult.class); verify(platformBillingResult, times(1)).success(resultCaptor.capture()); assertResultsMatch(resultCaptor.getValue(), billingResult); + verify(platformBillingResult, never()).error(any()); } @Test @@ -372,6 +376,7 @@ public void showAlternativeBillingOnlyInformationDialogSuccess() { ArgumentCaptor.forClass(PlatformBillingResult.class); verify(platformBillingResult, times(1)).success(resultCaptor.capture()); assertResultsMatch(resultCaptor.getValue(), billingResult); + verify(platformBillingResult, never()).error(any()); } @Test From 47f988c5d77f53d2728e6a822f2895c760920e64 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 7 Mar 2024 10:36:09 -0500 Subject: [PATCH 18/25] Partially convert queryPurchases, move some hard-coded logic from Java to Dart, and remove tests that no longer made sense --- .../plugins/inapppurchase/Messages.java | 128 ++++++++++++++++++ .../inapppurchase/MethodCallHandlerImpl.java | 30 ++-- .../plugins/inapppurchase/Translator.java | 6 +- .../inapppurchase/MethodCallHandlerTest.java | 42 +++--- .../plugins/inapppurchase/TranslatorTest.java | 6 +- .../billing_client_wrapper.dart | 10 +- .../lib/src/messages.g.dart | 66 +++++++++ .../lib/src/pigeon_converters.dart | 25 +++- .../pigeons/messages.dart | 29 ++++ .../billing_client_wrapper_test.dart | 55 +++----- .../billing_client_wrapper_test.mocks.dart | 54 ++++++-- ...rchase_android_platform_addition_test.dart | 72 +++------- ...in_app_purchase_android_platform_test.dart | 76 +++-------- 13 files changed, 392 insertions(+), 207 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java index 522945732c1..95c1ff2623c 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -705,6 +705,93 @@ ArrayList toList() { } } + /** + * Pigeon version of PurchasesResultWrapper, which contains the components of the Java + * PurchasesResponseListener callback. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PlatformPurchasesResponse { + private @NonNull PlatformBillingResult billingResult; + + public @NonNull PlatformBillingResult getBillingResult() { + return billingResult; + } + + public void setBillingResult(@NonNull PlatformBillingResult setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"billingResult\" is null."); + } + this.billingResult = setterArg; + } + + /** + * A JSON-compatible list of purchases, where each entry in the list is a Map + * JSON encoding of the product details. + */ + private @NonNull List purchasesJsonList; + + public @NonNull List getPurchasesJsonList() { + return purchasesJsonList; + } + + public void setPurchasesJsonList(@NonNull List setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"purchasesJsonList\" is null."); + } + this.purchasesJsonList = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PlatformPurchasesResponse() {} + + public static final class Builder { + + private @Nullable PlatformBillingResult billingResult; + + @CanIgnoreReturnValue + public @NonNull Builder setBillingResult(@NonNull PlatformBillingResult setterArg) { + this.billingResult = setterArg; + return this; + } + + private @Nullable List purchasesJsonList; + + @CanIgnoreReturnValue + public @NonNull Builder setPurchasesJsonList(@NonNull List setterArg) { + this.purchasesJsonList = setterArg; + return this; + } + + public @NonNull PlatformPurchasesResponse build() { + PlatformPurchasesResponse pigeonReturn = new PlatformPurchasesResponse(); + pigeonReturn.setBillingResult(billingResult); + pigeonReturn.setPurchasesJsonList(purchasesJsonList); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add((billingResult == null) ? null : billingResult.toList()); + toListResult.add(purchasesJsonList); + return toListResult; + } + + static @NonNull PlatformPurchasesResponse fromList(@NonNull ArrayList list) { + PlatformPurchasesResponse pigeonResult = new PlatformPurchasesResponse(); + Object billingResult = list.get(0); + pigeonResult.setBillingResult( + (billingResult == null) + ? null + : PlatformBillingResult.fromList((ArrayList) billingResult)); + Object purchasesJsonList = list.get(1); + pigeonResult.setPurchasesJsonList((List) purchasesJsonList); + return pigeonResult; + } + } + /** Asynchronous error handling return type for non-nullable API method returns. */ public interface Result { /** Success case callback method for handling returns. */ @@ -751,6 +838,8 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { return PlatformProduct.fromList((ArrayList) readValue(buffer)); case (byte) 133: return PlatformProductDetailsResponse.fromList((ArrayList) readValue(buffer)); + case (byte) 134: + return PlatformPurchasesResponse.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); } @@ -777,6 +866,9 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { } else if (value instanceof PlatformProductDetailsResponse) { stream.write(133); writeValue(stream, ((PlatformProductDetailsResponse) value).toList()); + } else if (value instanceof PlatformPurchasesResponse) { + stream.write(134); + writeValue(stream, ((PlatformPurchasesResponse) value).toList()); } else { super.writeValue(stream, value); } @@ -811,6 +903,10 @@ void acknowledgePurchase( @NonNull String purchaseToken, @NonNull Result result); /** Wraps BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener). */ void consumeAsync(@NonNull String purchaseToken, @NonNull Result result); + /** Wraps BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener). */ + void queryPurchasesAsync( + @NonNull PlatformProductType productType, + @NonNull Result result); /** * Wraps BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, * ProductDetailsResponseListener). @@ -1038,6 +1134,38 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.queryPurchasesAsync", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + PlatformProductType productTypeArg = + PlatformProductType.values()[(int) args.get(0)]; + Result resultCallback = + new Result() { + public void success(PlatformPurchasesResponse result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.queryPurchasesAsync(productTypeArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } { BasicMessageChannel channel = new BasicMessageChannel<>( diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index 39aefa0693d..02d4d9a30b3 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -12,6 +12,7 @@ import static io.flutter.plugins.inapppurchase.Translator.pigeonResultFromBillingConfig; import static io.flutter.plugins.inapppurchase.Translator.pigeonResultFromBillingResult; import static io.flutter.plugins.inapppurchase.Translator.toProductList; +import static io.flutter.plugins.inapppurchase.Translator.toProductTypeString; import android.app.Activity; import android.app.Application; @@ -42,6 +43,7 @@ import io.flutter.plugins.inapppurchase.Messages.PlatformBillingResult; import io.flutter.plugins.inapppurchase.Messages.PlatformProduct; import io.flutter.plugins.inapppurchase.Messages.PlatformProductDetailsResponse; +import io.flutter.plugins.inapppurchase.Messages.PlatformPurchasesResponse; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -56,8 +58,6 @@ class MethodCallHandlerImpl @VisibleForTesting static final class MethodNames { static final String ON_DISCONNECT = "BillingClientStateListener#onBillingServiceDisconnected()"; - static final String QUERY_PURCHASES_ASYNC = - "BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)"; static final String QUERY_PURCHASE_HISTORY_ASYNC = "BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener)"; @@ -140,9 +140,6 @@ void onDetachedFromActivity() { @Override public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { switch (call.method) { - case MethodNames.QUERY_PURCHASES_ASYNC: - queryPurchasesAsync((String) call.argument("productType"), result); - break; case MethodNames.QUERY_PURCHASE_HISTORY_ASYNC: queryPurchaseHistoryAsync((String) call.argument("productType"), result); break; @@ -380,26 +377,27 @@ public void consumeAsync( billingClient.consumeAsync(params, listener); } - private void queryPurchasesAsync(String productType, MethodChannel.Result result) { - if (billingClientError(result)) { + @Override + public void queryPurchasesAsync( + @NonNull Messages.PlatformProductType productType, + @NonNull Messages.Result result) { + if (billingClient == null) { + result.error(getNullBillingClientError()); return; } - assert billingClient != null; // Like in our connect call, consider the billing client responding a "success" here regardless // of status code. QueryPurchasesParams.Builder paramsBuilder = QueryPurchasesParams.newBuilder(); - paramsBuilder.setProductType(productType); + paramsBuilder.setProductType(toProductTypeString(productType)); billingClient.queryPurchasesAsync( paramsBuilder.build(), (billingResult, purchasesList) -> { - final Map serialized = new HashMap<>(); - // The response code is no longer passed, as part of billing 4.0, so we pass OK here - // as success is implied by calling this callback. - serialized.put("responseCode", BillingClient.BillingResponseCode.OK); - serialized.put("billingResult", fromBillingResult(billingResult)); - serialized.put("purchasesList", fromPurchasesList(purchasesList)); - result.success(serialized); + PlatformPurchasesResponse.Builder builder = + new PlatformPurchasesResponse.Builder() + .setBillingResult(pigeonResultFromBillingResult(billingResult)) + .setPurchasesJsonList(fromPurchasesList(purchasesList)); + result.success(builder.build()); }); } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java index 5a75ed90c53..c8b013c9b33 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java @@ -210,12 +210,14 @@ static HashMap fromPurchaseHistoryRecord( return info; } - static List> fromPurchasesList(@Nullable List purchases) { + static List fromPurchasesList(@Nullable List purchases) { if (purchases == null) { return Collections.emptyList(); } - List> serialized = new ArrayList<>(); + // This and the method are generically typed due to Pigeon limitations; see + // https://github.com/flutter/flutter/issues/116117. + List serialized = new ArrayList<>(); for (Purchase purchase : purchases) { serialized.add(fromPurchase(purchase)); } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index 095c9ca3880..4a34f55b9ca 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -6,7 +6,6 @@ import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.ACTIVITY_UNAVAILABLE; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.ON_DISCONNECT; -import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PURCHASES_ASYNC; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY; import static io.flutter.plugins.inapppurchase.PluginPurchaseListener.ON_PURCHASES_UPDATED; @@ -75,6 +74,7 @@ import io.flutter.plugins.inapppurchase.Messages.PlatformProduct; import io.flutter.plugins.inapppurchase.Messages.PlatformProductDetailsResponse; import io.flutter.plugins.inapppurchase.Messages.PlatformProductType; +import io.flutter.plugins.inapppurchase.Messages.PlatformPurchasesResponse; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; @@ -83,7 +83,6 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -109,6 +108,7 @@ public class MethodCallHandlerTest { @Spy Messages.Result platformBillingConfigResult; @Spy Messages.Result platformBillingResult; @Spy Messages.Result platformProductDetailsResult; + @Spy Messages.Result platformPurchasesResult; @Mock Activity activity; @Mock Context context; @@ -790,13 +790,15 @@ public void launchBillingFlow_oldProductNotFound() { public void queryPurchases_clientDisconnected() { methodChannelHandler.endConnection(); - HashMap arguments = new HashMap<>(); - arguments.put("type", BillingClient.ProductType.INAPP); - methodChannelHandler.onMethodCall(new MethodCall(QUERY_PURCHASES_ASYNC, arguments), result); + methodChannelHandler.queryPurchasesAsync(PlatformProductType.INAPP, platformPurchasesResult); - // Assert that we sent an error back. - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); - verify(result, never()).success(any()); + // Assert that the async call returns an error result. + verify(platformPurchasesResult, never()).success(any()); + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); + verify(platformPurchasesResult, times(1)).error(errorCaptor.capture()); + assertEquals("UNAVAILABLE", errorCaptor.getValue().code); + assertTrue( + Objects.requireNonNull(errorCaptor.getValue().getMessage()).contains("BillingClient")); } @Test @@ -831,23 +833,19 @@ public void queryPurchases_returns_success() throws Exception { .queryPurchasesAsync( any(QueryPurchasesParams.class), purchasesResponseListenerArgumentCaptor.capture()); - HashMap arguments = new HashMap<>(); - arguments.put("productType", BillingClient.ProductType.INAPP); - methodChannelHandler.onMethodCall(new MethodCall(QUERY_PURCHASES_ASYNC, arguments), result); + methodChannelHandler.queryPurchasesAsync(PlatformProductType.INAPP, platformPurchasesResult); - lock.await(5000, TimeUnit.MILLISECONDS); + verify(platformPurchasesResult, never()).error(any()); - verify(result, never()).error(any(), any(), any()); + ArgumentCaptor resultCaptor = + ArgumentCaptor.forClass(PlatformPurchasesResponse.class); + verify(platformPurchasesResult, times(1)).success(resultCaptor.capture()); - @SuppressWarnings("unchecked") - ArgumentCaptor> hashMapCaptor = ArgumentCaptor.forClass(HashMap.class); - verify(result, times(1)).success(hashMapCaptor.capture()); - - HashMap map = hashMapCaptor.getValue(); - assert (map.containsKey("responseCode")); - assert (map.containsKey("billingResult")); - assert (map.containsKey("purchasesList")); - assert ((int) map.get("responseCode") == 0); + PlatformPurchasesResponse purchasesResponse = resultCaptor.getValue(); + assertEquals( + purchasesResponse.getBillingResult().getResponseCode().longValue(), + BillingClient.BillingResponseCode.OK); + assertTrue(purchasesResponse.getPurchasesJsonList().isEmpty()); } @Test diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java index 40c6a0b2d4d..c5e8b143a0e 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java @@ -146,7 +146,7 @@ public void fromPurchasesList() throws JSONException { Arrays.asList( new Purchase(PURCHASE_EXAMPLE_JSON, signature), new Purchase(purchase2Json, signature)); - final List> serialized = Translator.fromPurchasesList(expected); + final List serialized = Translator.fromPurchasesList(expected); assertEquals(expected.size(), serialized.size()); assertSerialized(expected.get(0), serialized.get(0)); @@ -277,7 +277,9 @@ private void assertSerialized( assertEquals(expected.getRecurrenceMode(), serialized.get("recurrenceMode")); } - private void assertSerialized(Purchase expected, Map serialized) { + private void assertSerialized(Purchase expected, Object serializedGeneric) { + @SuppressWarnings("unchecked") + final Map serialized = (Map) serializedGeneric; assertEquals(expected.getOrderId(), serialized.get("orderId")); assertEquals(expected.getPackageName(), serialized.get("packageName")); assertEquals(expected.getPurchaseTime(), serialized.get("purchaseTime")); 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 5b7e98159eb..b93babc310f 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 @@ -230,14 +230,8 @@ class BillingClient { /// This wraps /// [`BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchasesAsync(com.android.billingclient.api.QueryPurchasesParams,%20com.android.billingclient.api.PurchasesResponseListener)). Future queryPurchases(ProductType productType) async { - return PurchasesResultWrapper.fromJson( - (await channel.invokeMapMethod( - 'BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)', - { - 'productType': const ProductTypeConverter().toJson(productType) - }, - )) ?? - {}); + return purchasesResultWrapperFromPlatform(await _hostApi + .queryPurchasesAsync(platformProductTypeFromWrapper(productType))); } /// Fetches purchase history for the given [ProductType]. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart index bcdb0a5e0e3..bab08763c6c 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -229,6 +229,36 @@ class PlatformBillingFlowParams { } } +/// Pigeon version of PurchasesResultWrapper, which contains the components of +/// the Java PurchasesResponseListener callback. +class PlatformPurchasesResponse { + PlatformPurchasesResponse({ + required this.billingResult, + required this.purchasesJsonList, + }); + + PlatformBillingResult billingResult; + + /// A JSON-compatible list of purchases, where each entry in the list is a + /// Map JSON encoding of the product details. + List purchasesJsonList; + + Object encode() { + return [ + billingResult.encode(), + purchasesJsonList, + ]; + } + + static PlatformPurchasesResponse decode(Object result) { + result as List; + return PlatformPurchasesResponse( + billingResult: PlatformBillingResult.decode(result[0]! as List), + purchasesJsonList: (result[1] as List?)!.cast(), + ); + } +} + class _InAppPurchaseApiCodec extends StandardMessageCodec { const _InAppPurchaseApiCodec(); @override @@ -251,6 +281,9 @@ class _InAppPurchaseApiCodec extends StandardMessageCodec { } else if (value is PlatformProductDetailsResponse) { buffer.putUint8(133); writeValue(buffer, value.encode()); + } else if (value is PlatformPurchasesResponse) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -272,6 +305,8 @@ class _InAppPurchaseApiCodec extends StandardMessageCodec { return PlatformProduct.decode(readValue(buffer)!); case 133: return PlatformProductDetailsResponse.decode(readValue(buffer)!); + case 134: + return PlatformPurchasesResponse.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -497,6 +532,37 @@ class InAppPurchaseApi { } } + /// Wraps BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener). + Future queryPurchasesAsync( + PlatformProductType productType) async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.queryPurchasesAsync'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = await __pigeon_channel + .send([productType.index]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as PlatformPurchasesResponse?)!; + } + } + /// Wraps BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener). Future queryProductDetailsAsync( List products) async { diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart index 6f5719cd3a2..e8f3653c03a 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart @@ -29,7 +29,7 @@ ProductDetailsResponseWrapper productDetailsResponseWrapperFromPlatform( PlatformProductDetailsResponse response) { return ProductDetailsResponseWrapper( billingResult: resultWrapperFromPlatform(response.billingResult), - // See TODOs in messages.dart for why this is JSON. + // See TODOs in messages.dart for why this is currently JSON. productDetailsList: response.productDetailsJsonList .map((Object? json) => ProductDetailsWrapper.fromJson( (json! as Map).cast())) @@ -37,6 +37,23 @@ ProductDetailsResponseWrapper productDetailsResponseWrapperFromPlatform( ); } +/// Creates a [PurchasesResultWrapper] from the Pigeon equivalent. +PurchasesResultWrapper purchasesResultWrapperFromPlatform( + PlatformPurchasesResponse response) { + return PurchasesResultWrapper( + billingResult: resultWrapperFromPlatform(response.billingResult), + // See TODOs in messages.dart for why this is currently JSON. + purchasesList: response.purchasesJsonList + .map((Object? json) => PurchaseWrapper.fromJson( + (json! as Map).cast())) + .toList(), + // This is no longer part of the response in current versions of the billing + // library, so use a success placeholder for compatibility with existing + // client code. + responseCode: BillingResponse.ok, + ); +} + /// Creates an [AlternativeBillingOnlyReportingDetailsWrapper] from the Pigeon /// equivalent. AlternativeBillingOnlyReportingDetailsWrapper @@ -50,6 +67,7 @@ AlternativeBillingOnlyReportingDetailsWrapper ); } +/// Creates a [BillingConfigWrapper] from the Pigeon equivalent. BillingConfigWrapper billingConfigWrapperFromPlatform( PlatformBillingConfigResponse response) { return BillingConfigWrapper( @@ -64,11 +82,12 @@ BillingConfigWrapper billingConfigWrapperFromPlatform( PlatformProduct platformProductFromWrapper(ProductWrapper product) { return PlatformProduct( productId: product.productId, - productType: _platformProductTypeFromWrapper(product.productType), + productType: platformProductTypeFromWrapper(product.productType), ); } -PlatformProductType _platformProductTypeFromWrapper(ProductType type) { +/// Converts a [ProductType] to its Pigeon equivalent. +PlatformProductType platformProductTypeFromWrapper(ProductType type) { return switch (type) { ProductType.inapp => PlatformProductType.inapp, ProductType.subs => PlatformProductType.subs, diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart index e1f1344520a..4789ec2242e 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -97,6 +97,30 @@ class PlatformBillingFlowParams { final String? purchaseToken; } +/// Pigeon version of PurchasesResultWrapper, which contains the components of +/// the Java PurchasesResponseListener callback. +class PlatformPurchasesResponse { + PlatformPurchasesResponse({ + required this.billingResult, + required this.purchasesJsonList, + }); + + final PlatformBillingResult billingResult; + + /// A JSON-compatible list of purchases, where each entry in the list is a + /// Map JSON encoding of the product details. + // TODO(stuartmorgan): Finish converting to Pigeon. This is still using the + // old serialization system to allow conversion of all the method calls to + // Pigeon without converting the entire object graph all at once. See + // https://github.com/flutter/flutter/issues/117910. The list items are + // currently untyped due to https://github.com/flutter/flutter/issues/116117. + // + // TODO(stuartmorgan): Make the generic type non-nullable once supported. + // https://github.com/flutter/flutter/issues/97848 + // The consuming code treats all of it as non-nullable. + final List purchasesJsonList; +} + /// Pigeon version of Java BillingClient.ProductType. enum PlatformProductType { inapp, @@ -142,6 +166,11 @@ abstract class InAppPurchaseApi { @async PlatformBillingResult consumeAsync(String purchaseToken); + /// Wraps BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener). + @async + PlatformPurchasesResponse queryPurchasesAsync( + PlatformProductType productType); + /// Wraps BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener). @async PlatformProductDetailsResponse queryProductDetailsAsync( diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index ce7ca1b039c..92610d408a5 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -360,9 +360,6 @@ void main() { }); group('queryPurchases', () { - const String queryPurchasesMethodName = - 'BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)'; - test('serializes and deserializes data', () async { const BillingResponse expectedCode = BillingResponse.ok; final List expectedList = [ @@ -371,14 +368,16 @@ void main() { const String debugMessage = 'dummy message'; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform - .addResponse(name: queryPurchasesMethodName, value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const BillingResponseConverter().toJson(expectedCode), - 'purchasesList': expectedList - .map((PurchaseWrapper purchase) => buildPurchaseMap(purchase)) - .toList(), - }); + when(mockApi.queryPurchasesAsync(any)).thenAnswer((_) async => + PlatformPurchasesResponse( + billingResult: PlatformBillingResult( + responseCode: + const BillingResponseConverter().toJson(expectedCode), + debugMessage: debugMessage), + purchasesJsonList: expectedList + .map((PurchaseWrapper purchase) => buildPurchaseMap(purchase)) + .toList(), + )); final PurchasesResultWrapper response = await billingClient.queryPurchases(ProductType.inapp); @@ -393,34 +392,22 @@ void main() { const String debugMessage = 'dummy message'; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform - .addResponse(name: queryPurchasesMethodName, value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const BillingResponseConverter().toJson(expectedCode), - 'purchasesList': [], - }); + when(mockApi.queryPurchasesAsync(any)) + .thenAnswer((_) async => PlatformPurchasesResponse( + billingResult: PlatformBillingResult( + responseCode: + const BillingResponseConverter().toJson(expectedCode), + debugMessage: debugMessage), + purchasesJsonList: >[], + )); final PurchasesResultWrapper response = await billingClient.queryPurchases(ProductType.inapp); expect(response.billingResult, equals(expectedBillingResult)); - expect(response.responseCode, equals(expectedCode)); - expect(response.purchasesList, isEmpty); - }); - - test('handles method channel returning null', () async { - stubPlatform.addResponse( - name: queryPurchasesMethodName, - ); - final PurchasesResultWrapper response = - await billingClient.queryPurchases(ProductType.inapp); - - expect( - response.billingResult, - equals(const BillingResultWrapper( - responseCode: BillingResponse.error, - debugMessage: kInvalidBillingResultErrorMessage))); - expect(response.responseCode, BillingResponse.error); + // The top-level response code is hard-coded to "ok", as the underlying + // API no longer returns it. + expect(response.responseCode, BillingResponse.ok); expect(response.purchasesList, isEmpty); }); }); diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart index a16f93f920b..d2ea52c9289 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart @@ -43,9 +43,20 @@ class _FakePlatformBillingConfigResponse_1 extends _i1.SmartFake ); } -class _FakePlatformProductDetailsResponse_2 extends _i1.SmartFake +class _FakePlatformPurchasesResponse_2 extends _i1.SmartFake + implements _i2.PlatformPurchasesResponse { + _FakePlatformPurchasesResponse_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformProductDetailsResponse_3 extends _i1.SmartFake implements _i2.PlatformProductDetailsResponse { - _FakePlatformProductDetailsResponse_2( + _FakePlatformProductDetailsResponse_3( Object parent, Invocation parentInvocation, ) : super( @@ -54,10 +65,10 @@ class _FakePlatformProductDetailsResponse_2 extends _i1.SmartFake ); } -class _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_3 +class _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_4 extends _i1.SmartFake implements _i2.PlatformAlternativeBillingOnlyReportingDetailsResponse { - _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_3( + _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_4( Object parent, Invocation parentInvocation, ) : super( @@ -230,6 +241,33 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { )), ) as _i3.Future<_i2.PlatformBillingResult>); + @override + _i3.Future<_i2.PlatformPurchasesResponse> queryPurchasesAsync( + _i2.PlatformProductType? productType) => + (super.noSuchMethod( + Invocation.method( + #queryPurchasesAsync, + [productType], + ), + returnValue: _i3.Future<_i2.PlatformPurchasesResponse>.value( + _FakePlatformPurchasesResponse_2( + this, + Invocation.method( + #queryPurchasesAsync, + [productType], + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.PlatformPurchasesResponse>.value( + _FakePlatformPurchasesResponse_2( + this, + Invocation.method( + #queryPurchasesAsync, + [productType], + ), + )), + ) as _i3.Future<_i2.PlatformPurchasesResponse>); + @override _i3.Future<_i2.PlatformProductDetailsResponse> queryProductDetailsAsync( List<_i2.PlatformProduct?>? products) => @@ -239,7 +277,7 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { [products], ), returnValue: _i3.Future<_i2.PlatformProductDetailsResponse>.value( - _FakePlatformProductDetailsResponse_2( + _FakePlatformProductDetailsResponse_3( this, Invocation.method( #queryProductDetailsAsync, @@ -248,7 +286,7 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { )), returnValueForMissingStub: _i3.Future<_i2.PlatformProductDetailsResponse>.value( - _FakePlatformProductDetailsResponse_2( + _FakePlatformProductDetailsResponse_3( this, Invocation.method( #queryProductDetailsAsync, @@ -330,7 +368,7 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { returnValue: _i3.Future< _i2 .PlatformAlternativeBillingOnlyReportingDetailsResponse>.value( - _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_3( + _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_4( this, Invocation.method( #createAlternativeBillingOnlyReportingDetailsAsync, @@ -340,7 +378,7 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { returnValueForMissingStub: _i3.Future< _i2 .PlatformAlternativeBillingOnlyReportingDetailsResponse>.value( - _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_3( + _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_4( this, Invocation.method( #createAlternativeBillingOnlyReportingDetailsAsync, 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 7a1c9ab481f..e07728d0a47 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 @@ -149,43 +149,20 @@ void main() { group('queryPastPurchase', () { group('queryPurchaseDetails', () { - const String queryMethodName = - 'BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)'; - test('handles error', () async { - const String debugMessage = 'dummy message'; - const BillingResponse responseCode = BillingResponse.developerError; - const BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - - stubPlatform - .addResponse(name: queryMethodName, value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const 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 ProductDetailsResponseWrapper', () async { const String debugMessage = 'dummy message'; const BillingResponse responseCode = BillingResponse.ok; - const BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - - stubPlatform - .addResponse(name: queryMethodName, value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const BillingResponseConverter().toJson(responseCode), - 'purchasesList': >[ - buildPurchaseMap(dummyPurchase), - ] - }); + + when(mockApi.queryPurchasesAsync(any)) + .thenAnswer((_) async => PlatformPurchasesResponse( + billingResult: PlatformBillingResult( + responseCode: + const BillingResponseConverter().toJson(responseCode), + debugMessage: debugMessage), + purchasesJsonList: >[ + buildPurchaseMap(dummyPurchase), + ], + )); // Since queryPastPurchases makes 2 platform method calls (one for each ProductType), the result will contain 2 dummyWrapper instead // of 1. @@ -196,26 +173,13 @@ void main() { }); test('should store platform exception in the response', () async { - const String debugMessage = 'dummy message'; - - const BillingResponse responseCode = BillingResponse.developerError; - const BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: queryMethodName, - value: { - 'responseCode': - const BillingResponseConverter().toJson(responseCode), - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'purchasesList': >[] - }, - additionalStepBeforeReturn: (dynamic _) { - throw PlatformException( - code: 'error_code', - message: 'error_message', - details: {'info': 'error_info'}, - ); - }); + when(mockApi.queryPurchasesAsync(any)).thenAnswer((_) async { + throw PlatformException( + code: 'error_code', + message: 'error_message', + details: {'info': 'error_info'}, + ); + }); final QueryPurchaseDetailsResponse response = await iapAndroidPlatformAddition.queryPastPurchases(); expect(response.pastPurchases, isEmpty); 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 a8bd218cfe4..3e17766bd8c 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 @@ -198,55 +198,14 @@ void main() { }); group('restorePurchases', () { - const String queryMethodName = - 'BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)'; - test('handles error', () async { - const String debugMessage = 'dummy message'; - const BillingResponse responseCode = BillingResponse.developerError; - const BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - - stubPlatform.addResponse(name: queryMethodName, value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const BillingResponseConverter().toJson(responseCode), - 'purchasesList': >[] - }); - - expect( - iapAndroidPlatform.restorePurchases(), - throwsA( - isA() - .having( - (InAppPurchaseException e) => e.source, 'source', kIAPSource) - .having((InAppPurchaseException e) => e.code, 'code', - kRestoredPurchaseErrorCode) - .having((InAppPurchaseException e) => e.message, 'message', - responseCode.toString()), - ), - ); - }); - test('should store platform exception in the response', () async { - const String debugMessage = 'dummy message'; - - const BillingResponse responseCode = BillingResponse.developerError; - const BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: queryMethodName, - value: { - 'responseCode': - const BillingResponseConverter().toJson(responseCode), - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'purchasesList': >[] - }, - additionalStepBeforeReturn: (dynamic _) { - throw PlatformException( - code: 'error_code', - message: 'error_message', - details: {'info': 'error_info'}, - ); - }); + when(mockApi.queryPurchasesAsync(any)).thenAnswer((_) async { + throw PlatformException( + code: 'error_code', + message: 'error_message', + details: {'info': 'error_info'}, + ); + }); expect( iapAndroidPlatform.restorePurchases(), @@ -277,16 +236,17 @@ void main() { const String debugMessage = 'dummy message'; const BillingResponse responseCode = BillingResponse.ok; - const BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - - stubPlatform.addResponse(name: queryMethodName, value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const BillingResponseConverter().toJson(responseCode), - 'purchasesList': >[ - buildPurchaseMap(dummyPurchase), - ] - }); + + when(mockApi.queryPurchasesAsync(any)) + .thenAnswer((_) async => PlatformPurchasesResponse( + billingResult: PlatformBillingResult( + responseCode: + const BillingResponseConverter().toJson(responseCode), + debugMessage: debugMessage), + purchasesJsonList: >[ + buildPurchaseMap(dummyPurchase), + ], + )); // Since queryPastPurchases makes 2 platform method calls (one for each // ProductType), the result will contain 2 dummyPurchase instances instead From 447863aa7779b4ece476da7a1eda5d4996318ea6 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 7 Mar 2024 12:02:36 -0500 Subject: [PATCH 19/25] Convert queryPurchaseHistoryAsync --- .../plugins/inapppurchase/Messages.java | 133 +++++++++++++++++- .../inapppurchase/MethodCallHandlerImpl.java | 34 +++-- .../plugins/inapppurchase/Translator.java | 6 +- .../inapppurchase/MethodCallHandlerTest.java | 36 ++--- .../plugins/inapppurchase/TranslatorTest.java | 8 +- .../billing_client_wrapper.dart | 10 +- .../lib/src/messages.g.dart | 69 ++++++++- .../lib/src/pigeon_converters.dart | 24 ++++ .../pigeons/messages.dart | 33 ++++- .../billing_client_wrapper_test.dart | 52 +++---- .../billing_client_wrapper_test.mocks.dart | 54 +++++-- 11 files changed, 366 insertions(+), 93 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java index 95c1ff2623c..ea1a6feded5 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -705,6 +705,93 @@ ArrayList toList() { } } + /** + * Pigeon version of PurchasesHistoryResult, which contains the components of the Java + * PurchaseHistoryResponseListener callback. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PlatformPurchaseHistoryResponse { + private @NonNull PlatformBillingResult billingResult; + + public @NonNull PlatformBillingResult getBillingResult() { + return billingResult; + } + + public void setBillingResult(@NonNull PlatformBillingResult setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"billingResult\" is null."); + } + this.billingResult = setterArg; + } + + /** + * A JSON-compatible list of purchase history records, where each entry in the list is a + * Map JSON encoding of the record. + */ + private @NonNull List purchaseHistoryRecordJsonList; + + public @NonNull List getPurchaseHistoryRecordJsonList() { + return purchaseHistoryRecordJsonList; + } + + public void setPurchaseHistoryRecordJsonList(@NonNull List setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"purchaseHistoryRecordJsonList\" is null."); + } + this.purchaseHistoryRecordJsonList = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PlatformPurchaseHistoryResponse() {} + + public static final class Builder { + + private @Nullable PlatformBillingResult billingResult; + + @CanIgnoreReturnValue + public @NonNull Builder setBillingResult(@NonNull PlatformBillingResult setterArg) { + this.billingResult = setterArg; + return this; + } + + private @Nullable List purchaseHistoryRecordJsonList; + + @CanIgnoreReturnValue + public @NonNull Builder setPurchaseHistoryRecordJsonList(@NonNull List setterArg) { + this.purchaseHistoryRecordJsonList = setterArg; + return this; + } + + public @NonNull PlatformPurchaseHistoryResponse build() { + PlatformPurchaseHistoryResponse pigeonReturn = new PlatformPurchaseHistoryResponse(); + pigeonReturn.setBillingResult(billingResult); + pigeonReturn.setPurchaseHistoryRecordJsonList(purchaseHistoryRecordJsonList); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add((billingResult == null) ? null : billingResult.toList()); + toListResult.add(purchaseHistoryRecordJsonList); + return toListResult; + } + + static @NonNull PlatformPurchaseHistoryResponse fromList(@NonNull ArrayList list) { + PlatformPurchaseHistoryResponse pigeonResult = new PlatformPurchaseHistoryResponse(); + Object billingResult = list.get(0); + pigeonResult.setBillingResult( + (billingResult == null) + ? null + : PlatformBillingResult.fromList((ArrayList) billingResult)); + Object purchaseHistoryRecordJsonList = list.get(1); + pigeonResult.setPurchaseHistoryRecordJsonList((List) purchaseHistoryRecordJsonList); + return pigeonResult; + } + } + /** * Pigeon version of PurchasesResultWrapper, which contains the components of the Java * PurchasesResponseListener callback. @@ -839,6 +926,8 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { case (byte) 133: return PlatformProductDetailsResponse.fromList((ArrayList) readValue(buffer)); case (byte) 134: + return PlatformPurchaseHistoryResponse.fromList((ArrayList) readValue(buffer)); + case (byte) 135: return PlatformPurchasesResponse.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -866,8 +955,11 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { } else if (value instanceof PlatformProductDetailsResponse) { stream.write(133); writeValue(stream, ((PlatformProductDetailsResponse) value).toList()); - } else if (value instanceof PlatformPurchasesResponse) { + } else if (value instanceof PlatformPurchaseHistoryResponse) { stream.write(134); + writeValue(stream, ((PlatformPurchaseHistoryResponse) value).toList()); + } else if (value instanceof PlatformPurchasesResponse) { + stream.write(135); writeValue(stream, ((PlatformPurchasesResponse) value).toList()); } else { super.writeValue(stream, value); @@ -907,6 +999,13 @@ void acknowledgePurchase( void queryPurchasesAsync( @NonNull PlatformProductType productType, @NonNull Result result); + /** + * Wraps BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, + * PurchaseHistoryResponseListener). + */ + void queryPurchaseHistoryAsync( + @NonNull PlatformProductType productType, + @NonNull Result result); /** * Wraps BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, * ProductDetailsResponseListener). @@ -1166,6 +1265,38 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.queryPurchaseHistoryAsync", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + PlatformProductType productTypeArg = + PlatformProductType.values()[(int) args.get(0)]; + Result resultCallback = + new Result() { + public void success(PlatformPurchaseHistoryResponse result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.queryPurchaseHistoryAsync(productTypeArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } { BasicMessageChannel channel = new BasicMessageChannel<>( diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index 02d4d9a30b3..34feeb54fef 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -4,7 +4,6 @@ package io.flutter.plugins.inapppurchase; -import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult; import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; @@ -43,6 +42,7 @@ import io.flutter.plugins.inapppurchase.Messages.PlatformBillingResult; import io.flutter.plugins.inapppurchase.Messages.PlatformProduct; import io.flutter.plugins.inapppurchase.Messages.PlatformProductDetailsResponse; +import io.flutter.plugins.inapppurchase.Messages.PlatformPurchaseHistoryResponse; import io.flutter.plugins.inapppurchase.Messages.PlatformPurchasesResponse; import java.util.ArrayList; import java.util.HashMap; @@ -58,8 +58,6 @@ class MethodCallHandlerImpl @VisibleForTesting static final class MethodNames { static final String ON_DISCONNECT = "BillingClientStateListener#onBillingServiceDisconnected()"; - static final String QUERY_PURCHASE_HISTORY_ASYNC = - "BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener)"; private MethodNames() {} } @@ -139,13 +137,7 @@ void onDetachedFromActivity() { @Override public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - switch (call.method) { - case MethodNames.QUERY_PURCHASE_HISTORY_ASYNC: - queryPurchaseHistoryAsync((String) call.argument("productType"), result); - break; - default: - result.notImplemented(); - } + result.notImplemented(); } @Override @@ -401,19 +393,25 @@ public void queryPurchasesAsync( }); } - private void queryPurchaseHistoryAsync(String productType, final MethodChannel.Result result) { - if (billingClientError(result)) { + @Override + public void queryPurchaseHistoryAsync( + @NonNull Messages.PlatformProductType productType, + @NonNull Messages.Result result) { + if (billingClient == null) { + result.error(getNullBillingClientError()); return; } - assert billingClient != null; billingClient.queryPurchaseHistoryAsync( - QueryPurchaseHistoryParams.newBuilder().setProductType(productType).build(), + QueryPurchaseHistoryParams.newBuilder() + .setProductType(toProductTypeString(productType)) + .build(), (billingResult, purchasesList) -> { - final Map serialized = new HashMap<>(); - serialized.put("billingResult", fromBillingResult(billingResult)); - serialized.put("purchaseHistoryRecordList", fromPurchaseHistoryRecordList(purchasesList)); - result.success(serialized); + PlatformPurchaseHistoryResponse.Builder builder = + new PlatformPurchaseHistoryResponse.Builder() + .setBillingResult(pigeonResultFromBillingResult(billingResult)) + .setPurchaseHistoryRecordJsonList(fromPurchaseHistoryRecordList(purchasesList)); + result.success(builder.build()); }); } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java index c8b013c9b33..874ec09e987 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java @@ -224,13 +224,15 @@ static List fromPurchasesList(@Nullable List purchases) { return serialized; } - static List> fromPurchaseHistoryRecordList( + static List fromPurchaseHistoryRecordList( @Nullable List purchaseHistoryRecords) { if (purchaseHistoryRecords == null) { return Collections.emptyList(); } - List> serialized = new ArrayList<>(); + // This and the method are generically typed due to Pigeon limitations; see + // https://github.com/flutter/flutter/issues/116117. + List serialized = new ArrayList<>(); for (PurchaseHistoryRecord purchaseHistoryRecord : purchaseHistoryRecords) { serialized.add(fromPurchaseHistoryRecord(purchaseHistoryRecord)); } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index 4a34f55b9ca..46c21cbce91 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -6,7 +6,6 @@ import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.ACTIVITY_UNAVAILABLE; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.ON_DISCONNECT; -import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY; import static io.flutter.plugins.inapppurchase.PluginPurchaseListener.ON_PURCHASES_UPDATED; import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult; @@ -23,7 +22,6 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.contains; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.refEq; import static org.mockito.Mockito.doAnswer; @@ -74,6 +72,7 @@ import io.flutter.plugins.inapppurchase.Messages.PlatformProduct; import io.flutter.plugins.inapppurchase.Messages.PlatformProductDetailsResponse; import io.flutter.plugins.inapppurchase.Messages.PlatformProductType; +import io.flutter.plugins.inapppurchase.Messages.PlatformPurchaseHistoryResponse; import io.flutter.plugins.inapppurchase.Messages.PlatformPurchasesResponse; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -108,6 +107,7 @@ public class MethodCallHandlerTest { @Spy Messages.Result platformBillingConfigResult; @Spy Messages.Result platformBillingResult; @Spy Messages.Result platformProductDetailsResult; + @Spy Messages.Result platformPurchaseHistoryResult; @Spy Messages.Result platformPurchasesResult; @Mock Activity activity; @@ -854,37 +854,39 @@ public void queryPurchaseHistoryAsync() { establishConnectedBillingClient(); BillingResult billingResult = buildBillingResult(); List purchasesList = singletonList(buildPurchaseHistoryRecord("foo")); - HashMap arguments = new HashMap<>(); - arguments.put("productType", BillingClient.ProductType.INAPP); ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(PurchaseHistoryResponseListener.class); - methodChannelHandler.onMethodCall( - new MethodCall(QUERY_PURCHASE_HISTORY_ASYNC, arguments), result); + methodChannelHandler.queryPurchaseHistoryAsync( + PlatformProductType.INAPP, platformPurchaseHistoryResult); // Verify we pass the data to result verify(mockBillingClient) .queryPurchaseHistoryAsync(any(QueryPurchaseHistoryParams.class), listenerCaptor.capture()); listenerCaptor.getValue().onPurchaseHistoryResponse(billingResult, purchasesList); - verify(result).success(resultCaptor.capture()); - HashMap resultData = resultCaptor.getValue(); - assertEquals(fromBillingResult(billingResult), resultData.get("billingResult")); + ArgumentCaptor resultCaptor = + ArgumentCaptor.forClass(PlatformPurchaseHistoryResponse.class); + verify(platformPurchaseHistoryResult).success(resultCaptor.capture()); + PlatformPurchaseHistoryResponse result = resultCaptor.getValue(); + assertResultsMatch(result.getBillingResult(), billingResult); assertEquals( - fromPurchaseHistoryRecordList(purchasesList), resultData.get("purchaseHistoryRecordList")); + fromPurchaseHistoryRecordList(purchasesList), result.getPurchaseHistoryRecordJsonList()); } @Test public void queryPurchaseHistoryAsync_clientDisconnected() { methodChannelHandler.endConnection(); - HashMap arguments = new HashMap<>(); - arguments.put("type", BillingClient.ProductType.INAPP); - methodChannelHandler.onMethodCall( - new MethodCall(QUERY_PURCHASE_HISTORY_ASYNC, arguments), result); + methodChannelHandler.queryPurchaseHistoryAsync( + PlatformProductType.INAPP, platformPurchaseHistoryResult); - // Assert that we sent an error back. - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); - verify(result, never()).success(any()); + // Assert that the async call returns an error result. + verify(platformPurchaseHistoryResult, never()).success(any()); + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); + verify(platformPurchaseHistoryResult, times(1)).error(errorCaptor.capture()); + assertEquals("UNAVAILABLE", errorCaptor.getValue().code); + assertTrue( + Objects.requireNonNull(errorCaptor.getValue().getMessage()).contains("BillingClient")); } @Test diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java index c5e8b143a0e..9ebb43694e2 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java @@ -21,7 +21,6 @@ import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -124,8 +123,7 @@ public void fromPurchasesHistoryRecordList() throws JSONException { new PurchaseHistoryRecord(PURCHASE_EXAMPLE_JSON, signature), new PurchaseHistoryRecord(purchase2Json, signature)); - final List> serialized = - Translator.fromPurchaseHistoryRecordList(expected); + final List serialized = Translator.fromPurchaseHistoryRecordList(expected); assertEquals(expected.size(), serialized.size()); assertSerialized(expected.get(0), serialized.get(0)); @@ -301,7 +299,9 @@ private void assertSerialized(Purchase expected, Object serializedGeneric) { serialized.get("obfuscatedProfileId")); } - private void assertSerialized(PurchaseHistoryRecord expected, Map serialized) { + private void assertSerialized(PurchaseHistoryRecord expected, Object serializedGeneric) { + @SuppressWarnings("unchecked") + final Map serialized = (Map) serializedGeneric; assertEquals(expected.getPurchaseTime(), serialized.get("purchaseTime")); assertEquals(expected.getPurchaseToken(), serialized.get("purchaseToken")); assertEquals(expected.getSignature(), serialized.get("signature")); 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 b93babc310f..ee1be33dd70 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 @@ -248,13 +248,9 @@ class BillingClient { /// [`BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchaseHistoryAsync(com.android.billingclient.api.QueryPurchaseHistoryParams,%20com.android.billingclient.api.PurchaseHistoryResponseListener)). Future queryPurchaseHistory( ProductType productType) async { - return PurchasesHistoryResult.fromJson((await channel.invokeMapMethod< - String, dynamic>( - 'BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener)', - { - 'productType': const ProductTypeConverter().toJson(productType) - })) ?? - {}); + return purchaseHistoryResultFromPlatform( + await _hostApi.queryPurchaseHistoryAsync( + platformProductTypeFromWrapper(productType))); } /// Consumes a given in-app product. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart index bab08763c6c..f4effcee991 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -229,6 +229,37 @@ class PlatformBillingFlowParams { } } +/// Pigeon version of PurchasesHistoryResult, which contains the components of +/// the Java PurchaseHistoryResponseListener callback. +class PlatformPurchaseHistoryResponse { + PlatformPurchaseHistoryResponse({ + required this.billingResult, + required this.purchaseHistoryRecordJsonList, + }); + + PlatformBillingResult billingResult; + + /// A JSON-compatible list of purchase history records, where each entry in + /// the list is a Map JSON encoding of the record. + List purchaseHistoryRecordJsonList; + + Object encode() { + return [ + billingResult.encode(), + purchaseHistoryRecordJsonList, + ]; + } + + static PlatformPurchaseHistoryResponse decode(Object result) { + result as List; + return PlatformPurchaseHistoryResponse( + billingResult: PlatformBillingResult.decode(result[0]! as List), + purchaseHistoryRecordJsonList: + (result[1] as List?)!.cast(), + ); + } +} + /// Pigeon version of PurchasesResultWrapper, which contains the components of /// the Java PurchasesResponseListener callback. class PlatformPurchasesResponse { @@ -281,9 +312,12 @@ class _InAppPurchaseApiCodec extends StandardMessageCodec { } else if (value is PlatformProductDetailsResponse) { buffer.putUint8(133); writeValue(buffer, value.encode()); - } else if (value is PlatformPurchasesResponse) { + } else if (value is PlatformPurchaseHistoryResponse) { buffer.putUint8(134); writeValue(buffer, value.encode()); + } else if (value is PlatformPurchasesResponse) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -306,6 +340,8 @@ class _InAppPurchaseApiCodec extends StandardMessageCodec { case 133: return PlatformProductDetailsResponse.decode(readValue(buffer)!); case 134: + return PlatformPurchaseHistoryResponse.decode(readValue(buffer)!); + case 135: return PlatformPurchasesResponse.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -563,6 +599,37 @@ class InAppPurchaseApi { } } + /// Wraps BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener). + Future queryPurchaseHistoryAsync( + PlatformProductType productType) async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.queryPurchaseHistoryAsync'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = await __pigeon_channel + .send([productType.index]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as PlatformPurchaseHistoryResponse?)!; + } + } + /// Wraps BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener). Future queryProductDetailsAsync( List products) async { diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart index e8f3653c03a..17bda283368 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart @@ -37,6 +37,19 @@ ProductDetailsResponseWrapper productDetailsResponseWrapperFromPlatform( ); } +/// Creates a [PurchaseHistoryResult] from the Pigeon equivalent. +PurchasesHistoryResult purchaseHistoryResultFromPlatform( + PlatformPurchaseHistoryResponse response) { + return PurchasesHistoryResult( + billingResult: resultWrapperFromPlatform(response.billingResult), + // See TODOs in messages.dart for why this is currently JSON. + purchaseHistoryRecordList: response.purchaseHistoryRecordJsonList + .map((Object? json) => PurchaseHistoryRecordWrapper.fromJson( + (json! as Map).cast())) + .toList(), + ); +} + /// Creates a [PurchasesResultWrapper] from the Pigeon equivalent. PurchasesResultWrapper purchasesResultWrapperFromPlatform( PlatformPurchasesResponse response) { @@ -50,6 +63,17 @@ PurchasesResultWrapper purchasesResultWrapperFromPlatform( // This is no longer part of the response in current versions of the billing // library, so use a success placeholder for compatibility with existing // client code. + // TODO(stuartmorgan): Investigate whether this is actually correct. This + // code preserves the behavior of the pre-Pigeon-conversion Java code, but + // the way this field is treated in PurchasesResultWrapper is inconsistent + // with ProductDetailsResponseWrapper and PurchasesHistoryResult, which have + // a getter for billingResult.responseCode instead of having a separate + // field. Several Dart unit tests had to be removed when this was moved from + // Java to Dart because they were testing a case that the plugin could never + // actually generate, and it may be that those tests were correct and the + // functionality they were intended to test had been broken by the original + // change to hard-code this on the Java side (instead of making it a + // forwarding getter on the Dart side). responseCode: BillingResponse.ok, ); } diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart index 4789ec2242e..207f98fdc8f 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -48,7 +48,7 @@ class PlatformProductDetailsResponse { // // TODO(stuartmorgan): Make the generic type non-nullable once supported. // https://github.com/flutter/flutter/issues/97848 - // The consuming code treats all of it as non-nullable. + // The consuming code treats it as non-nullable. final List productDetailsJsonList; } @@ -97,6 +97,30 @@ class PlatformBillingFlowParams { final String? purchaseToken; } +/// Pigeon version of PurchasesHistoryResult, which contains the components of +/// the Java PurchaseHistoryResponseListener callback. +class PlatformPurchaseHistoryResponse { + PlatformPurchaseHistoryResponse({ + required this.billingResult, + required this.purchaseHistoryRecordJsonList, + }); + + final PlatformBillingResult billingResult; + + /// A JSON-compatible list of purchase history records, where each entry in + /// the list is a Map JSON encoding of the record. + // TODO(stuartmorgan): Finish converting to Pigeon. This is still using the + // old serialization system to allow conversion of all the method calls to + // Pigeon without converting the entire object graph all at once. See + // https://github.com/flutter/flutter/issues/117910. The list items are + // currently untyped due to https://github.com/flutter/flutter/issues/116117. + // + // TODO(stuartmorgan): Make the generic type non-nullable once supported. + // https://github.com/flutter/flutter/issues/97848 + // The consuming code treats it as non-nullable. + final List purchaseHistoryRecordJsonList; +} + /// Pigeon version of PurchasesResultWrapper, which contains the components of /// the Java PurchasesResponseListener callback. class PlatformPurchasesResponse { @@ -117,7 +141,7 @@ class PlatformPurchasesResponse { // // TODO(stuartmorgan): Make the generic type non-nullable once supported. // https://github.com/flutter/flutter/issues/97848 - // The consuming code treats all of it as non-nullable. + // The consuming code treats it as non-nullable. final List purchasesJsonList; } @@ -171,6 +195,11 @@ abstract class InAppPurchaseApi { PlatformPurchasesResponse queryPurchasesAsync( PlatformProductType productType); + /// Wraps BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener). + @async + PlatformPurchaseHistoryResponse queryPurchaseHistoryAsync( + PlatformProductType productType); + /// Wraps BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener). @async PlatformProductDetailsResponse queryProductDetailsAsync( diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index 92610d408a5..323135a192a 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -413,9 +413,6 @@ void main() { }); group('queryPurchaseHistory', () { - const String queryPurchaseHistoryMethodName = - 'BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener)'; - test('serializes and deserializes data', () async { const BillingResponse expectedCode = BillingResponse.ok; final List expectedList = @@ -425,15 +422,17 @@ void main() { const String debugMessage = 'dummy message'; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: queryPurchaseHistoryMethodName, - value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'purchaseHistoryRecordList': expectedList - .map((PurchaseHistoryRecordWrapper purchaseHistoryRecord) => - buildPurchaseHistoryRecordMap(purchaseHistoryRecord)) - .toList(), - }); + when(mockApi.queryPurchaseHistoryAsync(any)) + .thenAnswer((_) async => PlatformPurchaseHistoryResponse( + billingResult: PlatformBillingResult( + responseCode: + const BillingResponseConverter().toJson(expectedCode), + debugMessage: debugMessage), + purchaseHistoryRecordJsonList: expectedList + .map((PurchaseHistoryRecordWrapper purchaseHistoryRecord) => + buildPurchaseHistoryRecordMap(purchaseHistoryRecord)) + .toList(), + )); final PurchasesHistoryResult response = await billingClient.queryPurchaseHistory(ProductType.inapp); @@ -446,12 +445,14 @@ void main() { const String debugMessage = 'dummy message'; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: queryPurchaseHistoryMethodName, - value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'purchaseHistoryRecordList': [], - }); + when(mockApi.queryPurchaseHistoryAsync(any)) + .thenAnswer((_) async => PlatformPurchaseHistoryResponse( + billingResult: PlatformBillingResult( + responseCode: + const BillingResponseConverter().toJson(expectedCode), + debugMessage: debugMessage), + purchaseHistoryRecordJsonList: >[], + )); final PurchasesHistoryResult response = await billingClient.queryPurchaseHistory(ProductType.inapp); @@ -459,21 +460,6 @@ void main() { expect(response.billingResult, equals(expectedBillingResult)); expect(response.purchaseHistoryRecordList, isEmpty); }); - - test('handles method channel returning null', () async { - stubPlatform.addResponse( - name: queryPurchaseHistoryMethodName, - ); - final PurchasesHistoryResult response = - await billingClient.queryPurchaseHistory(ProductType.inapp); - - expect( - response.billingResult, - equals(const BillingResultWrapper( - responseCode: BillingResponse.error, - debugMessage: kInvalidBillingResultErrorMessage))); - expect(response.purchaseHistoryRecordList, isEmpty); - }); }); group('consume purchases', () { diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart index d2ea52c9289..133c65445ba 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart @@ -54,9 +54,20 @@ class _FakePlatformPurchasesResponse_2 extends _i1.SmartFake ); } -class _FakePlatformProductDetailsResponse_3 extends _i1.SmartFake +class _FakePlatformPurchaseHistoryResponse_3 extends _i1.SmartFake + implements _i2.PlatformPurchaseHistoryResponse { + _FakePlatformPurchaseHistoryResponse_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformProductDetailsResponse_4 extends _i1.SmartFake implements _i2.PlatformProductDetailsResponse { - _FakePlatformProductDetailsResponse_3( + _FakePlatformProductDetailsResponse_4( Object parent, Invocation parentInvocation, ) : super( @@ -65,10 +76,10 @@ class _FakePlatformProductDetailsResponse_3 extends _i1.SmartFake ); } -class _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_4 +class _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_5 extends _i1.SmartFake implements _i2.PlatformAlternativeBillingOnlyReportingDetailsResponse { - _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_4( + _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_5( Object parent, Invocation parentInvocation, ) : super( @@ -268,6 +279,33 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { )), ) as _i3.Future<_i2.PlatformPurchasesResponse>); + @override + _i3.Future<_i2.PlatformPurchaseHistoryResponse> queryPurchaseHistoryAsync( + _i2.PlatformProductType? productType) => + (super.noSuchMethod( + Invocation.method( + #queryPurchaseHistoryAsync, + [productType], + ), + returnValue: _i3.Future<_i2.PlatformPurchaseHistoryResponse>.value( + _FakePlatformPurchaseHistoryResponse_3( + this, + Invocation.method( + #queryPurchaseHistoryAsync, + [productType], + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.PlatformPurchaseHistoryResponse>.value( + _FakePlatformPurchaseHistoryResponse_3( + this, + Invocation.method( + #queryPurchaseHistoryAsync, + [productType], + ), + )), + ) as _i3.Future<_i2.PlatformPurchaseHistoryResponse>); + @override _i3.Future<_i2.PlatformProductDetailsResponse> queryProductDetailsAsync( List<_i2.PlatformProduct?>? products) => @@ -277,7 +315,7 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { [products], ), returnValue: _i3.Future<_i2.PlatformProductDetailsResponse>.value( - _FakePlatformProductDetailsResponse_3( + _FakePlatformProductDetailsResponse_4( this, Invocation.method( #queryProductDetailsAsync, @@ -286,7 +324,7 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { )), returnValueForMissingStub: _i3.Future<_i2.PlatformProductDetailsResponse>.value( - _FakePlatformProductDetailsResponse_3( + _FakePlatformProductDetailsResponse_4( this, Invocation.method( #queryProductDetailsAsync, @@ -368,7 +406,7 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { returnValue: _i3.Future< _i2 .PlatformAlternativeBillingOnlyReportingDetailsResponse>.value( - _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_4( + _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_5( this, Invocation.method( #createAlternativeBillingOnlyReportingDetailsAsync, @@ -378,7 +416,7 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { returnValueForMissingStub: _i3.Future< _i2 .PlatformAlternativeBillingOnlyReportingDetailsResponse>.value( - _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_4( + _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_5( this, Invocation.method( #createAlternativeBillingOnlyReportingDetailsAsync, From 55a9541f6105ddb6f04eff47308edbd8a1b3ba18 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 7 Mar 2024 12:05:11 -0500 Subject: [PATCH 20/25] Remove old testing stub --- .../billing_client_wrapper_test.dart | 8 ---- ...rchase_android_platform_addition_test.dart | 10 ---- ...in_app_purchase_android_platform_test.dart | 12 ----- .../test/stub_in_app_purchase_platform.dart | 48 ------------------- 4 files changed, 78 deletions(-) delete mode 100644 packages/in_app_purchase/in_app_purchase_android/test/stub_in_app_purchase_platform.dart diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index 323135a192a..d0cd4fdf626 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -5,12 +5,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase_android/billing_client_wrappers.dart'; import 'package:in_app_purchase_android/src/billing_client_wrappers/billing_config_wrapper.dart'; -import 'package:in_app_purchase_android/src/channel.dart'; import 'package:in_app_purchase_android/src/messages.g.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import '../stub_in_app_purchase_platform.dart'; import '../test_conversion_utils.dart'; import 'billing_client_wrapper_test.mocks.dart'; import 'product_details_wrapper_test.dart'; @@ -34,20 +32,14 @@ const PurchaseWrapper dummyOldPurchase = PurchaseWrapper( void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); late MockInAppPurchaseApi mockApi; late BillingClient billingClient; - setUpAll(() => TestDefaultBinaryMessengerBinding - .instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, stubPlatform.fakeMethodCallHandler)); - setUp(() { mockApi = MockInAppPurchaseApi(); when(mockApi.startConnection(any, any)).thenAnswer( (_) async => PlatformBillingResult(responseCode: 0, debugMessage: '')); billingClient = BillingClient((PurchasesResultWrapper _) {}, api: mockApi); - stubPlatform.reset(); }); group('isReady', () { 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 e07728d0a47..fe6bcaa8370 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 @@ -8,31 +8,23 @@ 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/billing_config_wrapper.dart'; -import 'package:in_app_purchase_android/src/channel.dart'; import 'package:in_app_purchase_android/src/messages.g.dart'; import 'package:mockito/mockito.dart'; import 'billing_client_wrappers/billing_client_wrapper_test.dart'; import 'billing_client_wrappers/billing_client_wrapper_test.mocks.dart'; import 'billing_client_wrappers/purchase_wrapper_test.dart'; -import 'stub_in_app_purchase_platform.dart'; import 'test_conversion_utils.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); late MockInAppPurchaseApi mockApi; late InAppPurchaseAndroidPlatformAddition iapAndroidPlatformAddition; const String onBillingServiceDisconnectedCallback = 'BillingClientStateListener#onBillingServiceDisconnected()'; late BillingClientManager manager; - setUpAll(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, stubPlatform.fakeMethodCallHandler); - }); - setUp(() { widgets.WidgetsFlutterBinding.ensureInitialized(); mockApi = MockInAppPurchaseApi(); @@ -79,7 +71,6 @@ void main() { group('setBillingChoice', () { test('setAlternativeBillingOnlyState', () async { - stubPlatform.reset(); clearInteractions(mockApi); await iapAndroidPlatformAddition .setBillingChoice(BillingChoiceMode.alternativeBillingOnly); @@ -98,7 +89,6 @@ void main() { }); test('setPlayBillingState', () async { - stubPlatform.reset(); clearInteractions(mockApi); await iapAndroidPlatformAddition .setBillingChoice(BillingChoiceMode.playBillingOnly); 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 3e17766bd8c..c3b478f20e9 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 @@ -9,7 +9,6 @@ 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/channel.dart'; import 'package:in_app_purchase_android/src/messages.g.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import 'package:mockito/mockito.dart'; @@ -17,23 +16,16 @@ import 'package:mockito/mockito.dart'; import 'billing_client_wrappers/billing_client_wrapper_test.mocks.dart'; import 'billing_client_wrappers/product_details_wrapper_test.dart'; import 'billing_client_wrappers/purchase_wrapper_test.dart'; -import 'stub_in_app_purchase_platform.dart'; import 'test_conversion_utils.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); late MockInAppPurchaseApi mockApi; late InAppPurchaseAndroidPlatform iapAndroidPlatform; const String onBillingServiceDisconnectedCallback = 'BillingClientStateListener#onBillingServiceDisconnected()'; - setUpAll(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, stubPlatform.fakeMethodCallHandler); - }); - setUp(() { widgets.WidgetsFlutterBinding.ensureInitialized(); @@ -47,10 +39,6 @@ void main() { InAppPurchasePlatform.instance = iapAndroidPlatform; }); - tearDown(() { - stubPlatform.reset(); - }); - group('connection management', () { test('connects on initialization', () { //await iapAndroidPlatform.isAvailable(); 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 deleted file mode 100644 index 35e2807bc3b..00000000000 --- a/packages/in_app_purchase/in_app_purchase_android/test/stub_in_app_purchase_platform.dart +++ /dev/null @@ -1,48 +0,0 @@ -// 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'; - -// `FutureOr` instead of `FutureOr` to avoid -// "don't assign to void" warnings. -typedef AdditionalSteps = FutureOr Function(dynamic args); - -class StubInAppPurchasePlatform { - final Map _expectedCalls = {}; - final Map _additionalSteps = - {}; - void addResponse( - {required String name, - dynamic value, - AdditionalSteps? additionalStepBeforeReturn}) { - _additionalSteps[name] = additionalStepBeforeReturn; - _expectedCalls[name] = value; - } - - final List _previousCalls = []; - List get previousCalls => _previousCalls; - MethodCall previousCallMatching(String name) => - _previousCalls.firstWhere((MethodCall call) => call.method == name); - int countPreviousCalls(String name) => - _previousCalls.where((MethodCall call) => call.method == name).length; - - void reset() { - _expectedCalls.clear(); - _previousCalls.clear(); - _additionalSteps.clear(); - } - - Future fakeMethodCallHandler(MethodCall call) async { - _previousCalls.add(call); - if (_expectedCalls.containsKey(call.method)) { - if (_additionalSteps[call.method] != null) { - await _additionalSteps[call.method]!(call.arguments); - } - return Future.sync(() => _expectedCalls[call.method]); - } else { - return Future.sync(() => null); - } - } -} From b14eefb1831b3b4d7ac185954d7ed42ff03e79bd Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 7 Mar 2024 12:37:29 -0500 Subject: [PATCH 21/25] Java cleanup --- .../inapppurchase/InAppPurchasePlugin.java | 2 - .../inapppurchase/MethodCallHandlerImpl.java | 67 ++++++++----------- .../inapppurchase/PluginPurchaseListener.java | 4 +- .../plugins/inapppurchase/Translator.java | 12 ++-- .../inapppurchase/MethodCallHandlerTest.java | 22 ++---- .../plugins/inapppurchase/TranslatorTest.java | 12 ++-- 6 files changed, 50 insertions(+), 69 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java index a7872357cf1..e5887812ec0 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java @@ -75,13 +75,11 @@ private void setUpMethodChannel(BinaryMessenger messenger, Context context) { methodCallHandler = new MethodCallHandlerImpl( /*activity=*/ null, context, methodChannel, new BillingClientFactoryImpl()); - methodChannel.setMethodCallHandler(methodCallHandler); Messages.InAppPurchaseApi.setUp(messenger, methodCallHandler); } private void teardownMethodChannel(BinaryMessenger messenger) { Messages.InAppPurchaseApi.setUp(messenger, null); - methodChannel.setMethodCallHandler(null); methodChannel = null; methodCallHandler = null; } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index 34feeb54fef..dedd6a4138d 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -4,12 +4,12 @@ package io.flutter.plugins.inapppurchase; +import static io.flutter.plugins.inapppurchase.Translator.fromAlternativeBillingOnlyReportingDetails; +import static io.flutter.plugins.inapppurchase.Translator.fromBillingConfig; +import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult; import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; -import static io.flutter.plugins.inapppurchase.Translator.pigeonResultFromAlternativeBillingOnlyReportingDetails; -import static io.flutter.plugins.inapppurchase.Translator.pigeonResultFromBillingConfig; -import static io.flutter.plugins.inapppurchase.Translator.pigeonResultFromBillingResult; import static io.flutter.plugins.inapppurchase.Translator.toProductList; import static io.flutter.plugins.inapppurchase.Translator.toProductTypeString; @@ -33,7 +33,6 @@ import com.android.billingclient.api.QueryProductDetailsParams; import com.android.billingclient.api.QueryPurchaseHistoryParams; import com.android.billingclient.api.QueryPurchasesParams; -import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.inapppurchase.Messages.FlutterError; import io.flutter.plugins.inapppurchase.Messages.InAppPurchaseApi; @@ -42,18 +41,17 @@ import io.flutter.plugins.inapppurchase.Messages.PlatformBillingResult; import io.flutter.plugins.inapppurchase.Messages.PlatformProduct; import io.flutter.plugins.inapppurchase.Messages.PlatformProductDetailsResponse; +import io.flutter.plugins.inapppurchase.Messages.PlatformProductType; import io.flutter.plugins.inapppurchase.Messages.PlatformPurchaseHistoryResponse; import io.flutter.plugins.inapppurchase.Messages.PlatformPurchasesResponse; +import io.flutter.plugins.inapppurchase.Messages.Result; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** Handles method channel for the plugin. */ -class MethodCallHandlerImpl - implements MethodChannel.MethodCallHandler, - Application.ActivityLifecycleCallbacks, - InAppPurchaseApi { +class MethodCallHandlerImpl implements Application.ActivityLifecycleCallbacks, InAppPurchaseApi { @VisibleForTesting static final class MethodNames { @@ -135,14 +133,9 @@ void onDetachedFromActivity() { endBillingClientConnection(); } - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - result.notImplemented(); - } - @Override public void showAlternativeBillingOnlyInformationDialog( - @NonNull Messages.Result result) { + @NonNull Result result) { if (billingClient == null) { result.error(getNullBillingClientError()); return; @@ -152,13 +145,12 @@ public void showAlternativeBillingOnlyInformationDialog( return; } billingClient.showAlternativeBillingOnlyInformationDialog( - activity, billingResult -> result.success(pigeonResultFromBillingResult(billingResult))); + activity, billingResult -> result.success(fromBillingResult(billingResult))); } @Override public void createAlternativeBillingOnlyReportingDetailsAsync( - @NonNull - Messages.Result result) { + @NonNull Result result) { if (billingClient == null) { result.error(getNullBillingClientError()); return; @@ -166,24 +158,24 @@ public void createAlternativeBillingOnlyReportingDetailsAsync( billingClient.createAlternativeBillingOnlyReportingDetailsAsync( ((billingResult, alternativeBillingOnlyReportingDetails) -> result.success( - pigeonResultFromAlternativeBillingOnlyReportingDetails( + fromAlternativeBillingOnlyReportingDetails( billingResult, alternativeBillingOnlyReportingDetails)))); } @Override public void isAlternativeBillingOnlyAvailableAsync( - @NonNull Messages.Result result) { + @NonNull Result result) { if (billingClient == null) { result.error(getNullBillingClientError()); return; } billingClient.isAlternativeBillingOnlyAvailableAsync( - billingResult -> result.success(pigeonResultFromBillingResult(billingResult))); + billingResult -> result.success(fromBillingResult(billingResult))); } @Override public void getBillingConfigAsync( - @NonNull Messages.Result result) { + @NonNull Result result) { if (billingClient == null) { result.error(getNullBillingClientError()); return; @@ -191,7 +183,7 @@ public void getBillingConfigAsync( billingClient.getBillingConfigAsync( GetBillingConfigParams.newBuilder().build(), (billingResult, billingConfig) -> - result.success(pigeonResultFromBillingConfig(billingResult, billingConfig))); + result.success(fromBillingConfig(billingResult, billingConfig))); } @Override @@ -218,7 +210,7 @@ public Boolean isReady() { @Override public void queryProductDetailsAsync( @NonNull List products, - @NonNull Messages.Result result) { + @NonNull Result result) { if (billingClient == null) { result.error(getNullBillingClientError()); return; @@ -232,7 +224,7 @@ public void queryProductDetailsAsync( updateCachedProducts(productDetailsList); final PlatformProductDetailsResponse.Builder responseBuilder = new PlatformProductDetailsResponse.Builder() - .setBillingResult(pigeonResultFromBillingResult(billingResult)) + .setBillingResult(fromBillingResult(billingResult)) .setProductDetailsJsonList(fromProductDetailsList(productDetailsList)); result.success(responseBuilder.build()); }); @@ -338,8 +330,7 @@ public void queryProductDetailsAsync( subscriptionUpdateParamsBuilder, params.getProrationMode().intValue()); paramsBuilder.setSubscriptionUpdateParams(subscriptionUpdateParamsBuilder.build()); } - return pigeonResultFromBillingResult( - billingClient.launchBillingFlow(activity, paramsBuilder.build())); + return fromBillingResult(billingClient.launchBillingFlow(activity, paramsBuilder.build())); } // TODO(gmackall): Replace uses of deprecated setReplaceProrationMode. @@ -354,14 +345,14 @@ private void setReplaceProrationMode( @Override public void consumeAsync( - @NonNull String purchaseToken, @NonNull Messages.Result result) { + @NonNull String purchaseToken, @NonNull Result result) { if (billingClient == null) { result.error(getNullBillingClientError()); return; } ConsumeResponseListener listener = - (billingResult, outToken) -> result.success(pigeonResultFromBillingResult(billingResult)); + (billingResult, outToken) -> result.success(fromBillingResult(billingResult)); ConsumeParams.Builder paramsBuilder = ConsumeParams.newBuilder().setPurchaseToken(purchaseToken); ConsumeParams params = paramsBuilder.build(); @@ -371,8 +362,8 @@ public void consumeAsync( @Override public void queryPurchasesAsync( - @NonNull Messages.PlatformProductType productType, - @NonNull Messages.Result result) { + @NonNull PlatformProductType productType, + @NonNull Result result) { if (billingClient == null) { result.error(getNullBillingClientError()); return; @@ -387,7 +378,7 @@ public void queryPurchasesAsync( (billingResult, purchasesList) -> { PlatformPurchasesResponse.Builder builder = new PlatformPurchasesResponse.Builder() - .setBillingResult(pigeonResultFromBillingResult(billingResult)) + .setBillingResult(fromBillingResult(billingResult)) .setPurchasesJsonList(fromPurchasesList(purchasesList)); result.success(builder.build()); }); @@ -395,8 +386,8 @@ public void queryPurchasesAsync( @Override public void queryPurchaseHistoryAsync( - @NonNull Messages.PlatformProductType productType, - @NonNull Messages.Result result) { + @NonNull PlatformProductType productType, + @NonNull Result result) { if (billingClient == null) { result.error(getNullBillingClientError()); return; @@ -409,7 +400,7 @@ public void queryPurchaseHistoryAsync( (billingResult, purchasesList) -> { PlatformPurchaseHistoryResponse.Builder builder = new PlatformPurchaseHistoryResponse.Builder() - .setBillingResult(pigeonResultFromBillingResult(billingResult)) + .setBillingResult(fromBillingResult(billingResult)) .setPurchaseHistoryRecordJsonList(fromPurchaseHistoryRecordList(purchasesList)); result.success(builder.build()); }); @@ -419,7 +410,7 @@ public void queryPurchaseHistoryAsync( public void startConnection( @NonNull Long handle, @NonNull PlatformBillingChoiceMode billingMode, - @NonNull Messages.Result result) { + @NonNull Result result) { if (billingClient == null) { billingClient = billingClientFactory.createBillingClient(applicationContext, methodChannel, billingMode); @@ -438,7 +429,7 @@ public void onBillingSetupFinished(@NonNull BillingResult billingResult) { alreadyFinished = true; // Consider the fact that we've finished a success, leave it to the Dart side to // validate the responseCode. - result.success(pigeonResultFromBillingResult(billingResult)); + result.success(fromBillingResult(billingResult)); } @Override @@ -452,7 +443,7 @@ public void onBillingServiceDisconnected() { @Override public void acknowledgePurchase( - @NonNull String purchaseToken, @NonNull Messages.Result result) { + @NonNull String purchaseToken, @NonNull Result result) { if (billingClient == null) { result.error(getNullBillingClientError()); return; @@ -460,7 +451,7 @@ public void acknowledgePurchase( AcknowledgePurchaseParams params = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchaseToken).build(); billingClient.acknowledgePurchase( - params, billingResult -> result.success(pigeonResultFromBillingResult(billingResult))); + params, billingResult -> result.success(fromBillingResult(billingResult))); } protected void updateCachedProducts(@Nullable List productDetailsList) { diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/PluginPurchaseListener.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/PluginPurchaseListener.java index ce919f75d12..4f0a1242f0e 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/PluginPurchaseListener.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/PluginPurchaseListener.java @@ -4,8 +4,8 @@ package io.flutter.plugins.inapppurchase; -import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult; import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; +import static io.flutter.plugins.inapppurchase.Translator.mapFromBillingResult; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -33,7 +33,7 @@ class PluginPurchaseListener implements PurchasesUpdatedListener { public void onPurchasesUpdated( @NonNull BillingResult billingResult, @Nullable List purchases) { final Map callbackArgs = new HashMap<>(); - callbackArgs.put("billingResult", fromBillingResult(billingResult)); + callbackArgs.put("billingResult", mapFromBillingResult(billingResult)); callbackArgs.put("responseCode", billingResult.getResponseCode()); callbackArgs.put("purchasesList", fromPurchasesList(purchases)); channel.invokeMethod(ON_PURCHASES_UPDATED, callbackArgs); diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java index 874ec09e987..83d60203926 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java @@ -239,14 +239,14 @@ static List fromPurchaseHistoryRecordList( return serialized; } - static HashMap fromBillingResult(BillingResult billingResult) { + static HashMap mapFromBillingResult(BillingResult billingResult) { HashMap info = new HashMap<>(); info.put("responseCode", billingResult.getResponseCode()); info.put("debugMessage", billingResult.getDebugMessage()); return info; } - static Messages.PlatformBillingResult pigeonResultFromBillingResult(BillingResult billingResult) { + static Messages.PlatformBillingResult fromBillingResult(BillingResult billingResult) { return new Messages.PlatformBillingResult.Builder() .setResponseCode((long) billingResult.getResponseCode()) .setDebugMessage(billingResult.getDebugMessage()) @@ -254,10 +254,10 @@ static Messages.PlatformBillingResult pigeonResultFromBillingResult(BillingResul } /** Converter from {@link BillingResult} and {@link BillingConfig} to map. */ - static Messages.PlatformBillingConfigResponse pigeonResultFromBillingConfig( + static Messages.PlatformBillingConfigResponse fromBillingConfig( BillingResult result, BillingConfig billingConfig) { return new Messages.PlatformBillingConfigResponse.Builder() - .setBillingResult(pigeonResultFromBillingResult(result)) + .setBillingResult(fromBillingResult(result)) .setCountryCode(billingConfig.getCountryCode()) .build(); } @@ -266,10 +266,10 @@ static Messages.PlatformBillingConfigResponse pigeonResultFromBillingConfig( * Converter from {@link BillingResult} and {@link AlternativeBillingOnlyReportingDetails} to map. */ static Messages.PlatformAlternativeBillingOnlyReportingDetailsResponse - pigeonResultFromAlternativeBillingOnlyReportingDetails( + fromAlternativeBillingOnlyReportingDetails( BillingResult result, AlternativeBillingOnlyReportingDetails details) { return new Messages.PlatformAlternativeBillingOnlyReportingDetailsResponse.Builder() - .setBillingResult(pigeonResultFromBillingResult(result)) + .setBillingResult(fromBillingResult(result)) .setExternalTransactionToken(details.getExternalTransactionToken()) .build(); } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index 46c21cbce91..1858e93d6cb 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -8,10 +8,10 @@ import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.ON_DISCONNECT; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY; import static io.flutter.plugins.inapppurchase.PluginPurchaseListener.ON_PURCHASES_UPDATED; -import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult; import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; +import static io.flutter.plugins.inapppurchase.Translator.mapFromBillingResult; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.Collections.unmodifiableList; @@ -60,7 +60,6 @@ import com.android.billingclient.api.QueryPurchaseHistoryParams; import com.android.billingclient.api.QueryPurchasesParams; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugins.inapppurchase.Messages.FlutterError; @@ -86,7 +85,6 @@ import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; -import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; @@ -98,7 +96,6 @@ public class MethodCallHandlerTest { @Mock BillingClientFactory factory; @Mock BillingClient mockBillingClient; @Mock MethodChannel mockMethodChannel; - @Spy Result result; @Spy Messages.Result @@ -113,7 +110,6 @@ public class MethodCallHandlerTest { @Mock Activity activity; @Mock Context context; @Mock ActivityPluginBinding mockActivityPluginBinding; - @Captor ArgumentCaptor> resultCaptor; @Before public void setUp() { @@ -134,13 +130,6 @@ public void tearDown() throws Exception { openMocks.close(); } - @Test - public void invalidMethod() { - MethodCall call = new MethodCall("invalid", null); - methodChannelHandler.onMethodCall(call, result); - verify(result, times(1)).notImplemented(); - } - @Test public void isReady_true() { mockStartConnection(); @@ -802,9 +791,10 @@ public void queryPurchases_clientDisconnected() { } @Test - public void queryPurchases_returns_success() throws Exception { + public void queryPurchases_returns_success() { establishConnectedBillingClient(); + final Result mockResult = mock(Result.class); CountDownLatch lock = new CountDownLatch(1); doAnswer( (Answer) @@ -812,7 +802,7 @@ public void queryPurchases_returns_success() throws Exception { lock.countDown(); return null; }) - .when(result) + .when(mockResult) .success(any(HashMap.class)); ArgumentCaptor purchasesResponseListenerArgumentCaptor = @@ -895,13 +885,15 @@ public void onPurchasesUpdatedListener() { BillingResult billingResult = buildBillingResult(); List purchasesList = singletonList(buildPurchase("foo")); + @SuppressWarnings("unchecked") + ArgumentCaptor> resultCaptor = ArgumentCaptor.forClass(HashMap.class); doNothing() .when(mockMethodChannel) .invokeMethod(eq(ON_PURCHASES_UPDATED), resultCaptor.capture()); listener.onPurchasesUpdated(billingResult, purchasesList); HashMap resultData = resultCaptor.getValue(); - assertEquals(fromBillingResult(billingResult), resultData.get("billingResult")); + assertEquals(mapFromBillingResult(billingResult), resultData.get("billingResult")); assertEquals(fromPurchasesList(purchasesList), resultData.get("purchasesList")); } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java index 9ebb43694e2..d76f4958940 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java @@ -163,20 +163,20 @@ public void fromBillingResult() { .setDebugMessage("dummy debug message") .setResponseCode(BillingClient.BillingResponseCode.OK) .build(); - Map billingResultMap = Translator.fromBillingResult(newBillingResult); + Messages.PlatformBillingResult platformResult = Translator.fromBillingResult(newBillingResult); - assertEquals(billingResultMap.get("responseCode"), newBillingResult.getResponseCode()); - assertEquals(billingResultMap.get("debugMessage"), newBillingResult.getDebugMessage()); + assertEquals(platformResult.getResponseCode().longValue(), newBillingResult.getResponseCode()); + assertEquals(platformResult.getDebugMessage(), newBillingResult.getDebugMessage()); } @Test public void fromBillingResult_debugMessageNull() { BillingResult newBillingResult = BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.OK).build(); - Map billingResultMap = Translator.fromBillingResult(newBillingResult); + Messages.PlatformBillingResult platformResult = Translator.fromBillingResult(newBillingResult); - assertEquals(billingResultMap.get("responseCode"), newBillingResult.getResponseCode()); - assertEquals(billingResultMap.get("debugMessage"), newBillingResult.getDebugMessage()); + assertEquals(platformResult.getResponseCode().longValue(), newBillingResult.getResponseCode()); + assertEquals(platformResult.getDebugMessage(), newBillingResult.getDebugMessage()); } @Test From fb68b77ef9690545088ecef1f3147d6a4008e92a Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 11 Mar 2024 15:58:39 -0400 Subject: [PATCH 22/25] Replace the Java->Dart calls as well --- .../inapppurchase/BillingClientFactory.java | 5 +- .../BillingClientFactoryImpl.java | 5 +- .../inapppurchase/InAppPurchasePlugin.java | 7 +- .../plugins/inapppurchase/Messages.java | 106 +++++++++++++++++ .../inapppurchase/MethodCallHandlerImpl.java | 43 +++---- .../inapppurchase/PluginPurchaseListener.java | 37 +++--- .../plugins/inapppurchase/Translator.java | 7 -- .../inapppurchase/MethodCallHandlerTest.java | 55 +++------ .../billing_client_wrapper.dart | 104 ++++++++-------- .../lib/src/channel.dart | 9 -- .../lib/src/messages.g.dart | 111 ++++++++++++++++++ .../lib/src/pigeon_converters.dart | 22 +--- .../pigeons/messages.dart | 9 ++ .../billing_client_manager_test.dart | 14 +-- ...rchase_android_platform_addition_test.dart | 12 +- ...in_app_purchase_android_platform_test.dart | 102 +++++++--------- 16 files changed, 394 insertions(+), 254 deletions(-) delete mode 100644 packages/in_app_purchase/in_app_purchase_android/lib/src/channel.dart diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java index dbec5ed1a3c..979e54a37bb 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java @@ -7,7 +7,6 @@ import android.content.Context; import androidx.annotation.NonNull; import com.android.billingclient.api.BillingClient; -import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingChoiceMode; /** Responsible for creating a {@link BillingClient} object. */ @@ -17,13 +16,13 @@ interface BillingClientFactory { * Creates and returns a {@link BillingClient}. * * @param context The context used to create the {@link BillingClient}. - * @param channel The method channel used to create the {@link BillingClient}. + * @param callbackApi The callback API to be used by the {@link BillingClient}. * @param billingChoiceMode Enables the ability to offer alternative billing or Google Play * billing. * @return The {@link BillingClient} object that is created. */ BillingClient createBillingClient( @NonNull Context context, - @NonNull MethodChannel channel, + @NonNull Messages.InAppPurchaseCallbackApi callbackApi, PlatformBillingChoiceMode billingChoiceMode); } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java index 3ab1eef5dfd..c4b90024ac1 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java @@ -7,7 +7,6 @@ import android.content.Context; import androidx.annotation.NonNull; import com.android.billingclient.api.BillingClient; -import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingChoiceMode; /** The implementation for {@link BillingClientFactory} for the plugin. */ @@ -16,13 +15,13 @@ final class BillingClientFactoryImpl implements BillingClientFactory { @Override public BillingClient createBillingClient( @NonNull Context context, - @NonNull MethodChannel channel, + @NonNull Messages.InAppPurchaseCallbackApi callbackApi, PlatformBillingChoiceMode billingChoiceMode) { BillingClient.Builder builder = BillingClient.newBuilder(context).enablePendingPurchases(); if (billingChoiceMode == PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY) { // https://developer.android.com/google/play/billing/alternative/alternative-billing-without-user-choice-in-app builder.enableAlternativeBillingOnly(); } - return builder.setListener(new PluginPurchaseListener(channel)).build(); + return builder.setListener(new PluginPurchaseListener(callbackApi)).build(); } } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java index e5887812ec0..cc153dc4309 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java @@ -13,7 +13,6 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodChannel; /** Wraps a {@link BillingClient} instance and responds to Dart calls for it. */ public class InAppPurchasePlugin implements FlutterPlugin, ActivityAware { @@ -25,7 +24,6 @@ public class InAppPurchasePlugin implements FlutterPlugin, ActivityAware { // code owner of this package. static final String PROXY_VALUE = "io.flutter.plugins.inapppurchase"; - private MethodChannel methodChannel; private MethodCallHandlerImpl methodCallHandler; /** Plugin registration. */ @@ -71,16 +69,15 @@ public void onDetachedFromActivityForConfigChanges() { } private void setUpMethodChannel(BinaryMessenger messenger, Context context) { - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/in_app_purchase"); + Messages.InAppPurchaseCallbackApi handler = new Messages.InAppPurchaseCallbackApi(messenger); methodCallHandler = new MethodCallHandlerImpl( - /*activity=*/ null, context, methodChannel, new BillingClientFactoryImpl()); + /*activity=*/ null, context, handler, new BillingClientFactoryImpl()); Messages.InAppPurchaseApi.setUp(messenger, methodCallHandler); } private void teardownMethodChannel(BinaryMessenger messenger) { Messages.InAppPurchaseApi.setUp(messenger, null); - methodChannel = null; methodCallHandler = null; } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java index ea1a6feded5..7a0945c5576 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -21,6 +21,7 @@ import java.lang.annotation.Target; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** Generated class from Pigeon. */ @@ -60,6 +61,12 @@ protected static ArrayList wrapError(@NonNull Throwable exception) { return errorList; } + @NonNull + protected static FlutterError createConnectionError(@NonNull String channelName) { + return new FlutterError( + "channel-error", "Unable to establish connection on channel: " + channelName + ".", ""); + } + @Target(METHOD) @Retention(CLASS) @interface CanIgnoreReturnValue {} @@ -1443,4 +1450,103 @@ public void error(Throwable error) { } } } + + private static class InAppPurchaseCallbackApiCodec extends StandardMessageCodec { + public static final InAppPurchaseCallbackApiCodec INSTANCE = + new InAppPurchaseCallbackApiCodec(); + + private InAppPurchaseCallbackApiCodec() {} + + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return PlatformBillingResult.fromList((ArrayList) readValue(buffer)); + case (byte) 129: + return PlatformPurchasesResponse.fromList((ArrayList) readValue(buffer)); + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof PlatformBillingResult) { + stream.write(128); + writeValue(stream, ((PlatformBillingResult) value).toList()); + } else if (value instanceof PlatformPurchasesResponse) { + stream.write(129); + writeValue(stream, ((PlatformPurchasesResponse) value).toList()); + } else { + super.writeValue(stream, value); + } + } + } + + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class InAppPurchaseCallbackApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public InAppPurchaseCallbackApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + /** The codec used by InAppPurchaseCallbackApi. */ + static @NonNull MessageCodec getCodec() { + return InAppPurchaseCallbackApiCodec.INSTANCE; + } + /** Called for BillingClientStateListener#onBillingServiceDisconnected(). */ + public void onBillingServiceDisconnected( + @NonNull Long callbackHandleArg, @NonNull VoidResult result) { + final String channelName = + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseCallbackApi.onBillingServiceDisconnected"; + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, channelName, getCodec()); + channel.send( + new ArrayList(Collections.singletonList(callbackHandleArg)), + channelReply -> { + if (channelReply instanceof List) { + List listReply = (List) channelReply; + if (listReply.size() > 1) { + result.error( + new FlutterError( + (String) listReply.get(0), + (String) listReply.get(1), + (String) listReply.get(2))); + } else { + result.success(); + } + } else { + result.error(createConnectionError(channelName)); + } + }); + } + /** Called for PurchasesUpdatedListener#onPurchasesUpdated(BillingResult, List). */ + public void onPurchasesUpdated( + @NonNull PlatformPurchasesResponse updateArg, @NonNull VoidResult result) { + final String channelName = + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseCallbackApi.onPurchasesUpdated"; + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, channelName, getCodec()); + channel.send( + new ArrayList(Collections.singletonList(updateArg)), + channelReply -> { + if (channelReply instanceof List) { + List listReply = (List) channelReply; + if (listReply.size() > 1) { + result.error( + new FlutterError( + (String) listReply.get(0), + (String) listReply.get(1), + (String) listReply.get(2))); + } else { + result.success(); + } + } else { + result.error(createConnectionError(channelName)); + } + }); + } + } } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index dedd6a4138d..03d5886ecb7 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -33,9 +33,9 @@ import com.android.billingclient.api.QueryProductDetailsParams; import com.android.billingclient.api.QueryPurchaseHistoryParams; import com.android.billingclient.api.QueryPurchasesParams; -import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.inapppurchase.Messages.FlutterError; import io.flutter.plugins.inapppurchase.Messages.InAppPurchaseApi; +import io.flutter.plugins.inapppurchase.Messages.InAppPurchaseCallbackApi; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingChoiceMode; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingFlowParams; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingResult; @@ -48,18 +48,9 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Map; /** Handles method channel for the plugin. */ class MethodCallHandlerImpl implements Application.ActivityLifecycleCallbacks, InAppPurchaseApi { - - @VisibleForTesting - static final class MethodNames { - static final String ON_DISCONNECT = "BillingClientStateListener#onBillingServiceDisconnected()"; - - private MethodNames() {} - } - // TODO(gmackall): Replace uses of deprecated ProrationMode enum values with new // ReplacementMode enum values. // https://github.com/flutter/flutter/issues/128957. @@ -79,7 +70,7 @@ private MethodNames() {} @Nullable private Activity activity; private final Context applicationContext; - final MethodChannel methodChannel; + final InAppPurchaseCallbackApi callbackApi; private final HashMap cachedProducts = new HashMap<>(); @@ -87,12 +78,12 @@ private MethodNames() {} MethodCallHandlerImpl( @Nullable Activity activity, @NonNull Context applicationContext, - @NonNull MethodChannel methodChannel, + @NonNull InAppPurchaseCallbackApi callbackApi, @NonNull BillingClientFactory billingClientFactory) { this.billingClientFactory = billingClientFactory; this.applicationContext = applicationContext; this.activity = activity; - this.methodChannel = methodChannel; + this.callbackApi = callbackApi; } /** @@ -413,7 +404,7 @@ public void startConnection( @NonNull Result result) { if (billingClient == null) { billingClient = - billingClientFactory.createBillingClient(applicationContext, methodChannel, billingMode); + billingClientFactory.createBillingClient(applicationContext, callbackApi, billingMode); } billingClient.startConnection( @@ -434,9 +425,18 @@ public void onBillingSetupFinished(@NonNull BillingResult billingResult) { @Override public void onBillingServiceDisconnected() { - final Map arguments = new HashMap<>(); - arguments.put("handle", handle); - methodChannel.invokeMethod(MethodNames.ON_DISCONNECT, arguments); + callbackApi.onBillingServiceDisconnected( + handle, + new Messages.VoidResult() { + @Override + public void success() {} + + @Override + public void error(@NonNull Throwable error) { + io.flutter.Log.e( + "IN_APP_PURCHASE", "onBillingServiceDisconnected handler error: " + error); + } + }); } }); } @@ -464,15 +464,6 @@ protected void updateCachedProducts(@Nullable List productDetail } } - private boolean billingClientError(MethodChannel.Result result) { - if (billingClient != null) { - return false; - } - - result.error("UNAVAILABLE", "BillingClient is unset. Try reconnecting.", null); - return true; - } - private @NonNull FlutterError getNullBillingClientError() { return new FlutterError("UNAVAILABLE", "BillingClient is unset. Try reconnecting.", null); } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/PluginPurchaseListener.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/PluginPurchaseListener.java index 4f0a1242f0e..e8f1b40aeb5 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/PluginPurchaseListener.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/PluginPurchaseListener.java @@ -4,38 +4,41 @@ package io.flutter.plugins.inapppurchase; +import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult; import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; -import static io.flutter.plugins.inapppurchase.Translator.mapFromBillingResult; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.Purchase; import com.android.billingclient.api.PurchasesUpdatedListener; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; +import io.flutter.Log; import java.util.List; -import java.util.Map; class PluginPurchaseListener implements PurchasesUpdatedListener { - private final MethodChannel channel; + private final Messages.InAppPurchaseCallbackApi callbackApi; - @VisibleForTesting - static final String ON_PURCHASES_UPDATED = - "PurchasesUpdatedListener#onPurchasesUpdated(BillingResult, List)"; - - PluginPurchaseListener(MethodChannel channel) { - this.channel = channel; + PluginPurchaseListener(Messages.InAppPurchaseCallbackApi callbackApi) { + this.callbackApi = callbackApi; } @Override public void onPurchasesUpdated( @NonNull BillingResult billingResult, @Nullable List purchases) { - final Map callbackArgs = new HashMap<>(); - callbackArgs.put("billingResult", mapFromBillingResult(billingResult)); - callbackArgs.put("responseCode", billingResult.getResponseCode()); - callbackArgs.put("purchasesList", fromPurchasesList(purchases)); - channel.invokeMethod(ON_PURCHASES_UPDATED, callbackArgs); + Messages.PlatformPurchasesResponse.Builder builder = + new Messages.PlatformPurchasesResponse.Builder() + .setBillingResult(fromBillingResult(billingResult)) + .setPurchasesJsonList(fromPurchasesList(purchases)); + callbackApi.onPurchasesUpdated( + builder.build(), + new Messages.VoidResult() { + @Override + public void success() {} + + @Override + public void error(@NonNull Throwable error) { + Log.e("IN_APP_PURCHASE", "onPurchaseUpdated handler error: " + error); + } + }); } } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java index 83d60203926..73a867ce6f1 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java @@ -239,13 +239,6 @@ static List fromPurchaseHistoryRecordList( return serialized; } - static HashMap mapFromBillingResult(BillingResult billingResult) { - HashMap info = new HashMap<>(); - info.put("responseCode", billingResult.getResponseCode()); - info.put("debugMessage", billingResult.getDebugMessage()); - return info; - } - static Messages.PlatformBillingResult fromBillingResult(BillingResult billingResult) { return new Messages.PlatformBillingResult.Builder() .setResponseCode((long) billingResult.getResponseCode()) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index 1858e93d6cb..1db1d475f2f 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -5,13 +5,10 @@ package io.flutter.plugins.inapppurchase; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.ACTIVITY_UNAVAILABLE; -import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.ON_DISCONNECT; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY; -import static io.flutter.plugins.inapppurchase.PluginPurchaseListener.ON_PURCHASES_UPDATED; import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; -import static io.flutter.plugins.inapppurchase.Translator.mapFromBillingResult; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.Collections.unmodifiableList; @@ -60,9 +57,8 @@ import com.android.billingclient.api.QueryPurchaseHistoryParams; import com.android.billingclient.api.QueryPurchasesParams; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugins.inapppurchase.Messages.FlutterError; +import io.flutter.plugins.inapppurchase.Messages.InAppPurchaseCallbackApi; import io.flutter.plugins.inapppurchase.Messages.PlatformAlternativeBillingOnlyReportingDetailsResponse; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingChoiceMode; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingConfigResponse; @@ -76,11 +72,8 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.concurrent.CountDownLatch; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -95,7 +88,7 @@ public class MethodCallHandlerTest { private MethodCallHandlerImpl methodChannelHandler; @Mock BillingClientFactory factory; @Mock BillingClient mockBillingClient; - @Mock MethodChannel mockMethodChannel; + @Mock InAppPurchaseCallbackApi mockCallbackApi; @Spy Messages.Result @@ -116,12 +109,12 @@ public void setUp() { openMocks = MockitoAnnotations.openMocks(this); // Use the same client no matter if alternative billing is enabled or not. when(factory.createBillingClient( - context, mockMethodChannel, PlatformBillingChoiceMode.PLAY_BILLING_ONLY)) + context, mockCallbackApi, PlatformBillingChoiceMode.PLAY_BILLING_ONLY)) .thenReturn(mockBillingClient); when(factory.createBillingClient( - context, mockMethodChannel, PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY)) + context, mockCallbackApi, PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY)) .thenReturn(mockBillingClient); - methodChannelHandler = new MethodCallHandlerImpl(activity, context, mockMethodChannel, factory); + methodChannelHandler = new MethodCallHandlerImpl(activity, context, mockCallbackApi, factory); when(mockActivityPluginBinding.getActivity()).thenReturn(activity); } @@ -162,8 +155,7 @@ public void startConnection() { mockStartConnection(PlatformBillingChoiceMode.PLAY_BILLING_ONLY); verify(platformBillingResult, never()).success(any()); verify(factory, times(1)) - .createBillingClient( - context, mockMethodChannel, PlatformBillingChoiceMode.PLAY_BILLING_ONLY); + .createBillingClient(context, mockCallbackApi, PlatformBillingChoiceMode.PLAY_BILLING_ONLY); BillingResult billingResult = buildBillingResult(); captor.getValue().onBillingSetupFinished(billingResult); @@ -182,7 +174,7 @@ public void startConnectionAlternativeBillingOnly() { verify(platformBillingResult, never()).success(any()); verify(factory, times(1)) .createBillingClient( - context, mockMethodChannel, PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY); + context, mockCallbackApi, PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY); BillingResult billingResult = buildBillingResult(); captor.getValue().onBillingSetupFinished(billingResult); @@ -418,9 +410,9 @@ public void endConnection() { // been triggered verify(mockBillingClient, times(1)).endConnection(); stateListener.onBillingServiceDisconnected(); - Map expectedInvocation = new HashMap<>(); - expectedInvocation.put("handle", disconnectCallbackHandle); - verify(mockMethodChannel, times(1)).invokeMethod(ON_DISCONNECT, expectedInvocation); + ArgumentCaptor handleCaptor = ArgumentCaptor.forClass(Long.class); + verify(mockCallbackApi, times(1)).onBillingServiceDisconnected(handleCaptor.capture(), any()); + assertEquals(handleCaptor.getValue().longValue(), disconnectCallbackHandle); } @Test @@ -794,17 +786,6 @@ public void queryPurchases_clientDisconnected() { public void queryPurchases_returns_success() { establishConnectedBillingClient(); - final Result mockResult = mock(Result.class); - CountDownLatch lock = new CountDownLatch(1); - doAnswer( - (Answer) - invocation -> { - lock.countDown(); - return null; - }) - .when(mockResult) - .success(any(HashMap.class)); - ArgumentCaptor purchasesResponseListenerArgumentCaptor = ArgumentCaptor.forClass(PurchasesResponseListener.class); doAnswer( @@ -881,20 +862,18 @@ public void queryPurchaseHistoryAsync_clientDisconnected() { @Test public void onPurchasesUpdatedListener() { - PluginPurchaseListener listener = new PluginPurchaseListener(mockMethodChannel); + PluginPurchaseListener listener = new PluginPurchaseListener(mockCallbackApi); BillingResult billingResult = buildBillingResult(); List purchasesList = singletonList(buildPurchase("foo")); - @SuppressWarnings("unchecked") - ArgumentCaptor> resultCaptor = ArgumentCaptor.forClass(HashMap.class); - doNothing() - .when(mockMethodChannel) - .invokeMethod(eq(ON_PURCHASES_UPDATED), resultCaptor.capture()); + ArgumentCaptor resultCaptor = + ArgumentCaptor.forClass(PlatformPurchasesResponse.class); + doNothing().when(mockCallbackApi).onPurchasesUpdated(resultCaptor.capture(), any()); listener.onPurchasesUpdated(billingResult, purchasesList); - HashMap resultData = resultCaptor.getValue(); - assertEquals(mapFromBillingResult(billingResult), resultData.get("billingResult")); - assertEquals(fromPurchasesList(purchasesList), resultData.get("purchasesList")); + PlatformPurchasesResponse response = resultCaptor.getValue(); + assertResultsMatch(response.getBillingResult(), billingResult); + assertEquals(fromPurchasesList(purchasesList), response.getPurchasesJsonList()); } @Test 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 ee1be33dd70..477047338af 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 @@ -5,24 +5,15 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; import 'package:json_annotation/json_annotation.dart'; import '../../billing_client_wrappers.dart'; -import '../channel.dart'; import '../messages.g.dart'; import '../pigeon_converters.dart'; import 'billing_config_wrapper.dart'; part 'billing_client_wrapper.g.dart'; -/// Method identifier for the OnPurchaseUpdated method channel method. -@visibleForTesting -const String kOnPurchasesUpdated = - 'PurchasesUpdatedListener#onPurchasesUpdated(BillingResult, List)'; -const String _kOnBillingServiceDisconnected = - 'BillingClientStateListener#onBillingServiceDisconnected()'; - /// Callback triggered by Play in response to purchase activity. /// /// This callback is triggered in response to all purchase activity while an @@ -65,24 +56,18 @@ class BillingClient { BillingClient( PurchasesUpdatedListener onPurchasesUpdated, { @visibleForTesting InAppPurchaseApi? api, - }) : _hostApi = api ?? InAppPurchaseApi() { - channel.setMethodCallHandler(callHandler); - _callbacks[kOnPurchasesUpdated] = [ - onPurchasesUpdated - ]; + }) : _hostApi = api ?? InAppPurchaseApi(), + hostCallbackHandler = + HostBillingClientCallbackHandler(onPurchasesUpdated) { + InAppPurchaseCallbackApi.setup(hostCallbackHandler); } + /// Interface for calling host-side code. final InAppPurchaseApi _hostApi; - // Occasionally methods in the native layer require a Dart callback to be - // triggered in response to a Java callback. For example, - // [startConnection] registers an [OnBillingServiceDisconnected] callback. - // This list of names to callbacks is used to trigger Dart callbacks in - // response to those Java callbacks. Dart sends the Java layer a handle to the - // matching callback here to remember, and then once its twin is triggered it - // sends the handle back over the platform channel. We then access that handle - // in this array and call it in Dart code. See also [_callHandler]. - final Map> _callbacks = >{}; + /// Handlers for calls from the host-side code. + @visibleForTesting + final HostBillingClientCallbackHandler hostCallbackHandler; /// Calls /// [`BillingClient#isReady()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#isReady()) @@ -117,11 +102,9 @@ class BillingClient { {required OnBillingServiceDisconnected onBillingServiceDisconnected, BillingChoiceMode billingChoiceMode = BillingChoiceMode.playBillingOnly}) async { - final List disconnectCallbacks = - _callbacks[_kOnBillingServiceDisconnected] ??= []; - disconnectCallbacks.add(onBillingServiceDisconnected); + hostCallbackHandler.disconnectCallbacks.add(onBillingServiceDisconnected); return resultWrapperFromPlatform(await _hostApi.startConnection( - disconnectCallbacks.length - 1, + hostCallbackHandler.disconnectCallbacks.length - 1, platformBillingChoiceMode(billingChoiceMode))); } @@ -230,8 +213,24 @@ class BillingClient { /// This wraps /// [`BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchasesAsync(com.android.billingclient.api.QueryPurchasesParams,%20com.android.billingclient.api.PurchasesResponseListener)). Future queryPurchases(ProductType productType) async { - return purchasesResultWrapperFromPlatform(await _hostApi - .queryPurchasesAsync(platformProductTypeFromWrapper(productType))); + // TODO(stuartmorgan): Investigate whether forceOkResponseCode is actually + // correct. This code preserves the behavior of the pre-Pigeon-conversion + // Java code, but the way this field is treated in PurchasesResultWrapper is + // inconsistent with ProductDetailsResponseWrapper and + // PurchasesHistoryResult, which have a getter for + // billingResult.responseCode instead of having a separate field, and the + // other use of PurchasesResultWrapper (onPurchasesUpdated) was using + // billingResult.getResponseCode() for responseCode instead of hard-coding + // OK. Several Dart unit tests had to be removed when the hard-coding logic + // was moved from Java to here because they were testing a case that the + // plugin could never actually generate, and it may well be that those tests + // were correct and the functionality they were intended to test had been + // broken by the original change to hard-code this on the Java side (instead + // of making it a forwarding getter on the Dart side). + return purchasesResultWrapperFromPlatform( + await _hostApi + .queryPurchasesAsync(platformProductTypeFromWrapper(productType)), + forceOkResponseCode: true); } /// Fetches purchase history for the given [ProductType]. @@ -321,26 +320,35 @@ class BillingClient { return alternativeBillingOnlyReportingDetailsWrapperFromPlatform( await _hostApi.createAlternativeBillingOnlyReportingDetailsAsync()); } +} - /// The method call handler for [channel]. - @visibleForTesting - Future callHandler(MethodCall call) async { - switch (call.method) { - case kOnPurchasesUpdated: - // The purchases updated listener is a singleton. - assert(_callbacks[kOnPurchasesUpdated]!.length == 1); - final PurchasesUpdatedListener listener = - _callbacks[kOnPurchasesUpdated]!.first as PurchasesUpdatedListener; - listener(PurchasesResultWrapper.fromJson( - (call.arguments as Map).cast())); - case _kOnBillingServiceDisconnected: - final int handle = - (call.arguments as Map)['handle']! as int; - final List onDisconnected = - _callbacks[_kOnBillingServiceDisconnected]! - .cast(); - onDisconnected[handle](); - } +/// Implementation of InAppPurchaseCallbackApi, for use by [BillingClient]. +/// +/// Actual Dart callback functions are stored here, indexed by the handle +/// provided to the host side when setting up the connection in non-singleton +/// cases. When a callback is triggered from the host side, the corresponding +/// Dart function is invoked. +@visibleForTesting +class HostBillingClientCallbackHandler implements InAppPurchaseCallbackApi { + /// Creates a new handler with the given singleton handlers, and no + /// per-connection handlers. + HostBillingClientCallbackHandler(this.purchasesUpdatedCallback); + + /// The handler for PurchasesUpdatedListener#onPurchasesUpdated. + final PurchasesUpdatedListener purchasesUpdatedCallback; + + /// Handlers for onBillingServiceDisconnected, indexed by handle identifier. + final List disconnectCallbacks = + []; + + @override + void onBillingServiceDisconnected(int callbackHandle) { + disconnectCallbacks[callbackHandle](); + } + + @override + void onPurchasesUpdated(PlatformPurchasesResponse update) { + purchasesUpdatedCallback(purchasesResultWrapperFromPlatform(update)); } } diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/channel.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/channel.dart deleted file mode 100644 index f8ab4d48be7..00000000000 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/channel.dart +++ /dev/null @@ -1,9 +0,0 @@ -// 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:flutter/services.dart'; - -/// Method channel for the plugin's platform<-->Dart calls. -const MethodChannel channel = - MethodChannel('plugins.flutter.io/in_app_purchase'); diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart index f4effcee991..e45439b88b8 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -18,6 +18,17 @@ PlatformException _createConnectionError(String channelName) { ); } +List wrapResponse( + {Object? result, PlatformException? error, bool empty = false}) { + if (empty) { + return []; + } + if (error == null) { + return [result]; + } + return [error.code, error.message, error.details]; +} + /// Pigeon version of Java BillingClient.ProductType. enum PlatformProductType { inapp, @@ -784,3 +795,103 @@ class InAppPurchaseApi { } } } + +class _InAppPurchaseCallbackApiCodec extends StandardMessageCodec { + const _InAppPurchaseCallbackApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is PlatformBillingResult) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is PlatformPurchasesResponse) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return PlatformBillingResult.decode(readValue(buffer)!); + case 129: + return PlatformPurchasesResponse.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class InAppPurchaseCallbackApi { + static const MessageCodec pigeonChannelCodec = + _InAppPurchaseCallbackApiCodec(); + + /// Called for BillingClientStateListener#onBillingServiceDisconnected(). + void onBillingServiceDisconnected(int callbackHandle); + + /// Called for PurchasesUpdatedListener#onPurchasesUpdated(BillingResult, List). + void onPurchasesUpdated(PlatformPurchasesResponse update); + + static void setup(InAppPurchaseCallbackApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel __pigeon_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseCallbackApi.onBillingServiceDisconnected', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + __pigeon_channel.setMessageHandler(null); + } else { + __pigeon_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseCallbackApi.onBillingServiceDisconnected was null.'); + final List args = (message as List?)!; + final int? arg_callbackHandle = (args[0] as int?); + assert(arg_callbackHandle != null, + 'Argument for dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseCallbackApi.onBillingServiceDisconnected was null, expected non-null int.'); + try { + api.onBillingServiceDisconnected(arg_callbackHandle!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel __pigeon_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseCallbackApi.onPurchasesUpdated', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + __pigeon_channel.setMessageHandler(null); + } else { + __pigeon_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseCallbackApi.onPurchasesUpdated was null.'); + final List args = (message as List?)!; + final PlatformPurchasesResponse? arg_update = + (args[0] as PlatformPurchasesResponse?); + assert(arg_update != null, + 'Argument for dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseCallbackApi.onPurchasesUpdated was null, expected non-null PlatformPurchasesResponse.'); + try { + api.onPurchasesUpdated(arg_update!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart index 17bda283368..a7602f0b453 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart @@ -52,7 +52,8 @@ PurchasesHistoryResult purchaseHistoryResultFromPlatform( /// Creates a [PurchasesResultWrapper] from the Pigeon equivalent. PurchasesResultWrapper purchasesResultWrapperFromPlatform( - PlatformPurchasesResponse response) { + PlatformPurchasesResponse response, + {bool forceOkResponseCode = false}) { return PurchasesResultWrapper( billingResult: resultWrapperFromPlatform(response.billingResult), // See TODOs in messages.dart for why this is currently JSON. @@ -60,21 +61,10 @@ PurchasesResultWrapper purchasesResultWrapperFromPlatform( .map((Object? json) => PurchaseWrapper.fromJson( (json! as Map).cast())) .toList(), - // This is no longer part of the response in current versions of the billing - // library, so use a success placeholder for compatibility with existing - // client code. - // TODO(stuartmorgan): Investigate whether this is actually correct. This - // code preserves the behavior of the pre-Pigeon-conversion Java code, but - // the way this field is treated in PurchasesResultWrapper is inconsistent - // with ProductDetailsResponseWrapper and PurchasesHistoryResult, which have - // a getter for billingResult.responseCode instead of having a separate - // field. Several Dart unit tests had to be removed when this was moved from - // Java to Dart because they were testing a case that the plugin could never - // actually generate, and it may be that those tests were correct and the - // functionality they were intended to test had been broken by the original - // change to hard-code this on the Java side (instead of making it a - // forwarding getter on the Dart side). - responseCode: BillingResponse.ok, + responseCode: forceOkResponseCode + ? BillingResponse.ok + : const BillingResponseConverter() + .fromJson(response.billingResult.responseCode), ); } diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart index 207f98fdc8f..b95516e1034 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -224,3 +224,12 @@ abstract class InAppPurchaseApi { PlatformAlternativeBillingOnlyReportingDetailsResponse createAlternativeBillingOnlyReportingDetailsAsync(); } + +@FlutterApi() +abstract class InAppPurchaseCallbackApi { + /// Called for BillingClientStateListener#onBillingServiceDisconnected(). + void onBillingServiceDisconnected(int callbackHandle); + + /// Called for PurchasesUpdatedListener#onPurchasesUpdated(BillingResult, List). + void onPurchasesUpdated(PlatformPurchasesResponse update); +} 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 acc94b9a71f..e529d277f78 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 @@ -4,7 +4,6 @@ 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'; @@ -19,9 +18,6 @@ void main() { late MockInAppPurchaseApi mockApi; late BillingClientManager manager; - const String onBillingServiceDisconnectedCallback = - 'BillingClientStateListener#onBillingServiceDisconnected()'; - setUp(() { WidgetsFlutterBinding.ensureInitialized(); mockApi = MockInAppPurchaseApi(); @@ -65,10 +61,7 @@ void main() { // Ensures all asynchronous connected code finishes. await manager.runWithClientNonRetryable((_) async {}); - await manager.client.callHandler( - const MethodCall(onBillingServiceDisconnectedCallback, - {'handle': 0}), - ); + manager.client.hostCallbackHandler.onBillingServiceDisconnected(0); verify(mockApi.startConnection(any, any)).called(2); }); @@ -85,10 +78,7 @@ void main() { clearInteractions(mockApi); /// Fake the disconnect that we would expect from a endConnectionCall. - await manager.client.callHandler( - const MethodCall(onBillingServiceDisconnectedCallback, - {'handle': 0}), - ); + manager.client.hostCallbackHandler.onBillingServiceDisconnected(0); // Verify that after connection ended reconnect was called. final VerificationResult result = verify(mockApi.startConnection(any, captureAny)); 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 fe6bcaa8370..70973246e4d 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 @@ -21,8 +21,6 @@ void main() { late MockInAppPurchaseApi mockApi; late InAppPurchaseAndroidPlatformAddition iapAndroidPlatformAddition; - const String onBillingServiceDisconnectedCallback = - 'BillingClientStateListener#onBillingServiceDisconnected()'; late BillingClientManager manager; setUp(() { @@ -76,10 +74,7 @@ void main() { .setBillingChoice(BillingChoiceMode.alternativeBillingOnly); // Fake the disconnect that we would expect from a endConnectionCall. - await manager.client.callHandler( - const MethodCall(onBillingServiceDisconnectedCallback, - {'handle': 0}), - ); + manager.client.hostCallbackHandler.onBillingServiceDisconnected(0); // Verify that after connection ended reconnect was called. final VerificationResult result = verify(mockApi.startConnection(any, captureAny)); @@ -94,10 +89,7 @@ void main() { .setBillingChoice(BillingChoiceMode.playBillingOnly); // Fake the disconnect that we would expect from a endConnectionCall. - await manager.client.callHandler( - const MethodCall(onBillingServiceDisconnectedCallback, - {'handle': 0}), - ); + manager.client.hostCallbackHandler.onBillingServiceDisconnected(0); // Verify that after connection ended reconnect was called. final VerificationResult result = verify(mockApi.startConnection(any, captureAny)); 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 c3b478f20e9..38194bf8efb 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 @@ -23,8 +23,6 @@ void main() { late MockInAppPurchaseApi mockApi; late InAppPurchaseAndroidPlatform iapAndroidPlatform; - const String onBillingServiceDisconnectedCallback = - 'BillingClientStateListener#onBillingServiceDisconnected()'; setUp(() { widgets.WidgetsFlutterBinding.ensureInitialized(); @@ -46,10 +44,8 @@ void main() { }); test('re-connects when client sends onBillingServiceDisconnected', () { - iapAndroidPlatform.billingClientManager.client.callHandler( - const MethodCall(onBillingServiceDisconnectedCallback, - {'handle': 0}), - ); + iapAndroidPlatform.billingClientManager.client.hostCallbackHandler + .onBillingServiceDisconnected(0); verify(mockApi.startConnection(any, any)).called(2); }); @@ -272,11 +268,10 @@ void main() { when(mockApi.launchBillingFlow(any)).thenAnswer((_) async { // Mock java update purchase callback. - final MethodCall call = - MethodCall(kOnPurchasesUpdated, { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const BillingResponseConverter().toJson(sentCode), - 'purchasesList': [ + iapAndroidPlatform.billingClientManager.client.hostCallbackHandler + .onPurchasesUpdated(PlatformPurchasesResponse( + billingResult: convertToPigeonResult(expectedBillingResult), + purchasesJsonList: [ { 'orderId': 'orderID1', 'products': [productDetails.productId], @@ -290,9 +285,8 @@ void main() { 'isAcknowledged': true, 'purchaseState': 1, } - ] - }); - await iapAndroidPlatform.billingClientManager.client.callHandler(call); + ], + )); return convertToPigeonResult(expectedBillingResult); }); @@ -330,13 +324,11 @@ void main() { when(mockApi.launchBillingFlow(any)).thenAnswer((_) async { // Mock java update purchase callback. - final MethodCall call = - MethodCall(kOnPurchasesUpdated, { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const BillingResponseConverter().toJson(sentCode), - 'purchasesList': const [] - }); - await iapAndroidPlatform.billingClientManager.client.callHandler(call); + iapAndroidPlatform.billingClientManager.client.hostCallbackHandler + .onPurchasesUpdated(PlatformPurchasesResponse( + billingResult: convertToPigeonResult(expectedBillingResult), + purchasesJsonList: [], + )); return convertToPigeonResult(expectedBillingResult); }); @@ -374,11 +366,10 @@ void main() { when(mockApi.launchBillingFlow(any)).thenAnswer((_) async { // Mock java update purchase callback. - final MethodCall call = - MethodCall(kOnPurchasesUpdated, { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const BillingResponseConverter().toJson(sentCode), - 'purchasesList': [ + iapAndroidPlatform.billingClientManager.client.hostCallbackHandler + .onPurchasesUpdated(PlatformPurchasesResponse( + billingResult: convertToPigeonResult(expectedBillingResult), + purchasesJsonList: [ { 'orderId': 'orderID1', 'products': [productDetails.productId], @@ -392,9 +383,8 @@ void main() { 'isAcknowledged': true, 'purchaseState': 1, } - ] - }); - await iapAndroidPlatform.billingClientManager.client.callHandler(call); + ], + )); return convertToPigeonResult(expectedBillingResult); }); @@ -486,11 +476,10 @@ void main() { responseCode: sentCode, debugMessage: debugMessage); when(mockApi.launchBillingFlow(any)).thenAnswer((_) async { // Mock java update purchase callback. - final MethodCall call = - MethodCall(kOnPurchasesUpdated, { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const BillingResponseConverter().toJson(sentCode), - 'purchasesList': [ + iapAndroidPlatform.billingClientManager.client.hostCallbackHandler + .onPurchasesUpdated(PlatformPurchasesResponse( + billingResult: convertToPigeonResult(expectedBillingResult), + purchasesJsonList: [ { 'orderId': 'orderID1', 'products': [productDetails.productId], @@ -504,9 +493,8 @@ void main() { 'isAcknowledged': true, 'purchaseState': 1, } - ] - }); - await iapAndroidPlatform.billingClientManager.client.callHandler(call); + ], + )); return convertToPigeonResult(expectedBillingResult); }); @@ -562,11 +550,10 @@ void main() { when(mockApi.launchBillingFlow(any)).thenAnswer((_) async { // Mock java update purchase callback. - final MethodCall call = - MethodCall(kOnPurchasesUpdated, { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const BillingResponseConverter().toJson(sentCode), - 'purchasesList': [ + iapAndroidPlatform.billingClientManager.client.hostCallbackHandler + .onPurchasesUpdated(PlatformPurchasesResponse( + billingResult: convertToPigeonResult(expectedBillingResult), + purchasesJsonList: [ { 'orderId': 'orderID1', 'products': [productDetails.productId], @@ -580,9 +567,8 @@ void main() { 'isAcknowledged': true, 'purchaseState': 1, } - ] - }); - await iapAndroidPlatform.billingClientManager.client.callHandler(call); + ], + )); return convertToPigeonResult(expectedBillingResult); }); @@ -626,11 +612,10 @@ void main() { responseCode: sentCode, debugMessage: debugMessage); when(mockApi.launchBillingFlow(any)).thenAnswer((_) async { // Mock java update purchase callback. - final MethodCall call = - MethodCall(kOnPurchasesUpdated, { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const BillingResponseConverter().toJson(sentCode), - 'purchasesList': [ + iapAndroidPlatform.billingClientManager.client.hostCallbackHandler + .onPurchasesUpdated(PlatformPurchasesResponse( + billingResult: convertToPigeonResult(expectedBillingResult), + purchasesJsonList: [ { 'orderId': 'orderID1', 'products': [productDetails.productId], @@ -644,9 +629,8 @@ void main() { 'isAcknowledged': true, 'purchaseState': 1, } - ] - }); - await iapAndroidPlatform.billingClientManager.client.callHandler(call); + ], + )); return convertToPigeonResult(expectedBillingResult); }); @@ -696,13 +680,11 @@ void main() { responseCode: sentCode, debugMessage: debugMessage); when(mockApi.launchBillingFlow(any)).thenAnswer((_) async { // Mock java update purchase callback. - final MethodCall call = - MethodCall(kOnPurchasesUpdated, { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': const BillingResponseConverter().toJson(sentCode), - 'purchasesList': const [] - }); - await iapAndroidPlatform.billingClientManager.client.callHandler(call); + iapAndroidPlatform.billingClientManager.client.hostCallbackHandler + .onPurchasesUpdated(PlatformPurchasesResponse( + billingResult: convertToPigeonResult(expectedBillingResult), + purchasesJsonList: [], + )); return convertToPigeonResult(expectedBillingResult); }); From 770f92b729af01019fbae93e2f3926f520172f2c Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 12 Mar 2024 13:47:25 -0400 Subject: [PATCH 23/25] Version bump --- packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md | 4 ++++ packages/in_app_purchase/in_app_purchase_android/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 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 223d0441a78..4b7329ca448 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.3.2+1 + +* Converts internal platform communication to Pigeon. + ## 0.3.2 * Adds UserChoiceBilling APIs to platform addition. 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 8f2f1cd9674..54da00c680f 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/packages/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.3.2 +version: 0.3.2+1 environment: sdk: ^3.1.0 From 4ba0b406a00b7525e54aaec0110e459ab76c6516 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 12 Mar 2024 14:39:05 -0400 Subject: [PATCH 24/25] Add exception handlers to async methods --- .../inapppurchase/MethodCallHandlerImpl.java | 219 +++++++++++------- 1 file changed, 130 insertions(+), 89 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index f61b5631cf5..c9543f13286 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -137,8 +137,12 @@ public void showAlternativeBillingOnlyInformationDialog( result.error(new FlutterError(ACTIVITY_UNAVAILABLE, "Not attempting to show dialog", null)); return; } - billingClient.showAlternativeBillingOnlyInformationDialog( - activity, billingResult -> result.success(fromBillingResult(billingResult))); + try { + billingClient.showAlternativeBillingOnlyInformationDialog( + activity, billingResult -> result.success(fromBillingResult(billingResult))); + } catch (RuntimeException e) { + result.error(new FlutterError("error", e.getMessage(), Log.getStackTraceString(e))); + } } @Override @@ -148,11 +152,15 @@ public void createAlternativeBillingOnlyReportingDetailsAsync( result.error(getNullBillingClientError()); return; } - billingClient.createAlternativeBillingOnlyReportingDetailsAsync( - ((billingResult, alternativeBillingOnlyReportingDetails) -> - result.success( - fromAlternativeBillingOnlyReportingDetails( - billingResult, alternativeBillingOnlyReportingDetails)))); + try { + billingClient.createAlternativeBillingOnlyReportingDetailsAsync( + ((billingResult, alternativeBillingOnlyReportingDetails) -> + result.success( + fromAlternativeBillingOnlyReportingDetails( + billingResult, alternativeBillingOnlyReportingDetails)))); + } catch (RuntimeException e) { + result.error(new FlutterError("error", e.getMessage(), Log.getStackTraceString(e))); + } } @Override @@ -162,8 +170,12 @@ public void isAlternativeBillingOnlyAvailableAsync( result.error(getNullBillingClientError()); return; } - billingClient.isAlternativeBillingOnlyAvailableAsync( - billingResult -> result.success(fromBillingResult(billingResult))); + try { + billingClient.isAlternativeBillingOnlyAvailableAsync( + billingResult -> result.success(fromBillingResult(billingResult))); + } catch (RuntimeException e) { + result.error(new FlutterError("error", e.getMessage(), Log.getStackTraceString(e))); + } } @Override @@ -173,10 +185,14 @@ public void getBillingConfigAsync( result.error(getNullBillingClientError()); return; } - billingClient.getBillingConfigAsync( - GetBillingConfigParams.newBuilder().build(), - (billingResult, billingConfig) -> - result.success(fromBillingConfig(billingResult, billingConfig))); + try { + billingClient.getBillingConfigAsync( + GetBillingConfigParams.newBuilder().build(), + (billingResult, billingConfig) -> + result.success(fromBillingConfig(billingResult, billingConfig))); + } catch (RuntimeException e) { + result.error(new FlutterError("error", e.getMessage(), Log.getStackTraceString(e))); + } } @Override @@ -209,18 +225,22 @@ public void queryProductDetailsAsync( return; } - QueryProductDetailsParams params = - QueryProductDetailsParams.newBuilder().setProductList(toProductList(products)).build(); - billingClient.queryProductDetailsAsync( - params, - (billingResult, productDetailsList) -> { - updateCachedProducts(productDetailsList); - final PlatformProductDetailsResponse.Builder responseBuilder = - new PlatformProductDetailsResponse.Builder() - .setBillingResult(fromBillingResult(billingResult)) - .setProductDetailsJsonList(fromProductDetailsList(productDetailsList)); - result.success(responseBuilder.build()); - }); + try { + QueryProductDetailsParams params = + QueryProductDetailsParams.newBuilder().setProductList(toProductList(products)).build(); + billingClient.queryProductDetailsAsync( + params, + (billingResult, productDetailsList) -> { + updateCachedProducts(productDetailsList); + final PlatformProductDetailsResponse.Builder responseBuilder = + new PlatformProductDetailsResponse.Builder() + .setBillingResult(fromBillingResult(billingResult)) + .setProductDetailsJsonList(fromProductDetailsList(productDetailsList)); + result.success(responseBuilder.build()); + }); + } catch (RuntimeException e) { + result.error(new FlutterError("error", e.getMessage(), Log.getStackTraceString(e))); + } } @Override @@ -344,13 +364,17 @@ public void consumeAsync( return; } - ConsumeResponseListener listener = - (billingResult, outToken) -> result.success(fromBillingResult(billingResult)); - ConsumeParams.Builder paramsBuilder = - ConsumeParams.newBuilder().setPurchaseToken(purchaseToken); - ConsumeParams params = paramsBuilder.build(); + try { + ConsumeResponseListener listener = + (billingResult, outToken) -> result.success(fromBillingResult(billingResult)); + ConsumeParams.Builder paramsBuilder = + ConsumeParams.newBuilder().setPurchaseToken(purchaseToken); + ConsumeParams params = paramsBuilder.build(); - billingClient.consumeAsync(params, listener); + billingClient.consumeAsync(params, listener); + } catch (RuntimeException e) { + result.error(new FlutterError("error", e.getMessage(), Log.getStackTraceString(e))); + } } @Override @@ -362,19 +386,23 @@ public void queryPurchasesAsync( return; } - // Like in our connect call, consider the billing client responding a "success" here regardless - // of status code. - QueryPurchasesParams.Builder paramsBuilder = QueryPurchasesParams.newBuilder(); - paramsBuilder.setProductType(toProductTypeString(productType)); - billingClient.queryPurchasesAsync( - paramsBuilder.build(), - (billingResult, purchasesList) -> { - PlatformPurchasesResponse.Builder builder = - new PlatformPurchasesResponse.Builder() - .setBillingResult(fromBillingResult(billingResult)) - .setPurchasesJsonList(fromPurchasesList(purchasesList)); - result.success(builder.build()); - }); + try { + // Like in our connect call, consider the billing client responding a "success" here regardless + // of status code. + QueryPurchasesParams.Builder paramsBuilder = QueryPurchasesParams.newBuilder(); + paramsBuilder.setProductType(toProductTypeString(productType)); + billingClient.queryPurchasesAsync( + paramsBuilder.build(), + (billingResult, purchasesList) -> { + PlatformPurchasesResponse.Builder builder = + new PlatformPurchasesResponse.Builder() + .setBillingResult(fromBillingResult(billingResult)) + .setPurchasesJsonList(fromPurchasesList(purchasesList)); + result.success(builder.build()); + }); + } catch (RuntimeException e) { + result.error(new FlutterError("error", e.getMessage(), Log.getStackTraceString(e))); + } } @Override @@ -386,17 +414,21 @@ public void queryPurchaseHistoryAsync( return; } - billingClient.queryPurchaseHistoryAsync( - QueryPurchaseHistoryParams.newBuilder() - .setProductType(toProductTypeString(productType)) - .build(), - (billingResult, purchasesList) -> { - PlatformPurchaseHistoryResponse.Builder builder = - new PlatformPurchaseHistoryResponse.Builder() - .setBillingResult(fromBillingResult(billingResult)) - .setPurchaseHistoryRecordJsonList(fromPurchaseHistoryRecordList(purchasesList)); - result.success(builder.build()); - }); + try { + billingClient.queryPurchaseHistoryAsync( + QueryPurchaseHistoryParams.newBuilder() + .setProductType(toProductTypeString(productType)) + .build(), + (billingResult, purchasesList) -> { + PlatformPurchaseHistoryResponse.Builder builder = + new PlatformPurchaseHistoryResponse.Builder() + .setBillingResult(fromBillingResult(billingResult)) + .setPurchaseHistoryRecordJsonList(fromPurchaseHistoryRecordList(purchasesList)); + result.success(builder.build()); + }); + } catch (RuntimeException e) { + result.error(new FlutterError("error", e.getMessage(), Log.getStackTraceString(e))); + } } @Override @@ -411,38 +443,43 @@ public void startConnection( applicationContext, callbackApi, billingMode, listener); } - billingClient.startConnection( - new BillingClientStateListener() { - private boolean alreadyFinished = false; + try { + billingClient.startConnection( + new BillingClientStateListener() { + private boolean alreadyFinished = false; + + @Override + public void onBillingSetupFinished(@NonNull BillingResult billingResult) { + if (alreadyFinished) { + Log.d(TAG, "Tried to call onBillingSetupFinished multiple times."); + return; + } + alreadyFinished = true; + // Consider the fact that we've finished a success, leave it to the Dart side to + // validate the responseCode. + result.success(fromBillingResult(billingResult)); + } - @Override - public void onBillingSetupFinished(@NonNull BillingResult billingResult) { - if (alreadyFinished) { - Log.d(TAG, "Tried to call onBillingSetupFinished multiple times."); - return; + @Override + public void onBillingServiceDisconnected() { + callbackApi.onBillingServiceDisconnected( + handle, + new Messages.VoidResult() { + @Override + public void success() {} + + @Override + public void error(@NonNull Throwable error) { + io.flutter.Log.e( + "IN_APP_PURCHASE", + "onBillingServiceDisconnected handler error: " + error); + } + }); } - alreadyFinished = true; - // Consider the fact that we've finished a success, leave it to the Dart side to - // validate the responseCode. - result.success(fromBillingResult(billingResult)); - } - - @Override - public void onBillingServiceDisconnected() { - callbackApi.onBillingServiceDisconnected( - handle, - new Messages.VoidResult() { - @Override - public void success() {} - - @Override - public void error(@NonNull Throwable error) { - io.flutter.Log.e( - "IN_APP_PURCHASE", "onBillingServiceDisconnected handler error: " + error); - } - }); - } - }); + }); + } catch (RuntimeException e) { + result.error(new FlutterError("error", e.getMessage(), Log.getStackTraceString(e))); + } } @Nullable @@ -476,10 +513,14 @@ public void acknowledgePurchase( result.error(getNullBillingClientError()); return; } - AcknowledgePurchaseParams params = - AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchaseToken).build(); - billingClient.acknowledgePurchase( - params, billingResult -> result.success(fromBillingResult(billingResult))); + try { + AcknowledgePurchaseParams params = + AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchaseToken).build(); + billingClient.acknowledgePurchase( + params, billingResult -> result.success(fromBillingResult(billingResult))); + } catch (RuntimeException e) { + result.error(new FlutterError("error", e.getMessage(), Log.getStackTraceString(e))); + } } protected void updateCachedProducts(@Nullable List productDetailsList) { From 356aad1218bfddbd9ef6b92096bb37b7522d6672 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 14 Mar 2024 10:18:09 -0400 Subject: [PATCH 25/25] Nullability fix --- .../io/flutter/plugins/inapppurchase/Messages.java | 11 ++++------- .../in_app_purchase_android/lib/src/messages.g.dart | 6 +++--- .../lib/src/pigeon_converters.dart | 2 +- .../in_app_purchase_android/pigeons/messages.dart | 2 +- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java index 9650269af23..09701660aa9 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -894,16 +894,13 @@ ArrayList toList() { *

Generated class from Pigeon that represents data sent in messages. */ public static final class PlatformUserChoiceDetails { - private @NonNull String originalExternalTransactionId; + private @Nullable String originalExternalTransactionId; - public @NonNull String getOriginalExternalTransactionId() { + public @Nullable String getOriginalExternalTransactionId() { return originalExternalTransactionId; } - public void setOriginalExternalTransactionId(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"originalExternalTransactionId\" is null."); - } + public void setOriginalExternalTransactionId(@Nullable String setterArg) { this.originalExternalTransactionId = setterArg; } @@ -945,7 +942,7 @@ public static final class Builder { private @Nullable String originalExternalTransactionId; @CanIgnoreReturnValue - public @NonNull Builder setOriginalExternalTransactionId(@NonNull String setterArg) { + public @NonNull Builder setOriginalExternalTransactionId(@Nullable String setterArg) { this.originalExternalTransactionId = setterArg; return this; } diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart index 4b625ec3b9a..6f19a7b92a8 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -307,12 +307,12 @@ class PlatformPurchasesResponse { /// Pigeon version of UserChoiceDetailsWrapper and Java UserChoiceDetails. class PlatformUserChoiceDetails { PlatformUserChoiceDetails({ - required this.originalExternalTransactionId, + this.originalExternalTransactionId, required this.externalTransactionToken, required this.productsJsonList, }); - String originalExternalTransactionId; + String? originalExternalTransactionId; String externalTransactionToken; @@ -331,7 +331,7 @@ class PlatformUserChoiceDetails { static PlatformUserChoiceDetails decode(Object result) { result as List; return PlatformUserChoiceDetails( - originalExternalTransactionId: result[0]! as String, + originalExternalTransactionId: result[0] as String?, externalTransactionToken: result[1]! as String, productsJsonList: (result[2] as List?)!.cast(), ); diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart index bb4b11cc51e..232302f3a5f 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart @@ -114,7 +114,7 @@ PlatformProductType platformProductTypeFromWrapper(ProductType type) { UserChoiceDetailsWrapper userChoiceDetailsFromPlatform( PlatformUserChoiceDetails details) { return UserChoiceDetailsWrapper( - originalExternalTransactionId: details.originalExternalTransactionId, + originalExternalTransactionId: details.originalExternalTransactionId ?? '', externalTransactionToken: details.externalTransactionToken, // See TODOs in messages.dart for why this is currently JSON. products: details.productsJsonList diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart index 38767088835..7d66a8723d9 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -153,7 +153,7 @@ class PlatformUserChoiceDetails { required this.productsJsonList, }); - final String originalExternalTransactionId; + final String? originalExternalTransactionId; final String externalTransactionToken; /// A JSON-compatible list of products, where each entry in the list is a