From 0e848facdb53b0901fce6f84ead1b1ea95c0f42c Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 3 Apr 2024 10:49:09 -0400 Subject: [PATCH] [in_app_purchase] Mostly convert to Android Pigeon (#6262) Replaces manual method channels with Pigeon. This replaces all of the method calls in both direction with Pigeon calls, and converts all the top-level objects used for params and returns to Pigeon objects. However, because of the significant object graph, in order to somewhat limit the scope of this PR I made a cut at that layer in the object graph, with nested objects still using the existing JSON serialization. In a follow-up PR, those will be converted to typed Pigeon objects as well, completing the transition. Unfortunately a significant amount of JSON code can't be removed yet even though it's now unused by the plugin, because it's part of the API of the public wrappers, so clients may be using it. Once all the JSON code is unused, we could `@Deprecated` all of it, and then could do a follow-up breaking change later to remove it. Most of https://github.com/flutter/flutter/issues/117910 --- .../in_app_purchase_android/CHANGELOG.md | 4 + .../inapppurchase/BillingClientFactory.java | 8 +- .../BillingClientFactoryImpl.java | 15 +- .../inapppurchase/InAppPurchasePlugin.java | 15 +- .../plugins/inapppurchase/Messages.java | 1690 +++++++++++++++++ .../inapppurchase/MethodCallHandlerImpl.java | 638 +++---- .../inapppurchase/PluginPurchaseListener.java | 35 +- .../plugins/inapppurchase/Translator.java | 102 +- .../inapppurchase/MethodCallHandlerTest.java | 1044 +++++----- .../plugins/inapppurchase/TranslatorTest.java | 37 +- .../billing_client_manager.dart | 26 +- .../billing_client_wrapper.dart | 295 ++- .../billing_response_wrapper.dart | 2 + .../user_choice_details_wrapper.dart | 1 - .../lib/src/channel.dart | 9 - .../src/in_app_purchase_android_platform.dart | 12 +- .../lib/src/messages.g.dart | 971 ++++++++++ .../lib/src/pigeon_converters.dart | 125 ++ .../pigeons/copyright.txt | 3 + .../pigeons/messages.dart | 266 +++ .../in_app_purchase_android/pubspec.yaml | 3 +- .../billing_client_manager_test.dart | 91 +- .../billing_client_wrapper_test.dart | 613 ++---- .../billing_client_wrapper_test.mocks.dart | 428 +++++ ...rchase_android_platform_addition_test.dart | 204 +- ...in_app_purchase_android_platform_test.dart | 630 +++--- .../test/stub_in_app_purchase_platform.dart | 48 - .../test/test_conversion_utils.dart | 19 + 28 files changed, 5092 insertions(+), 2242 deletions(-) create mode 100644 packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java delete mode 100644 packages/in_app_purchase/in_app_purchase_android/lib/src/channel.dart 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/lib/src/pigeon_converters.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 delete mode 100644 packages/in_app_purchase/in_app_purchase_android/test/stub_in_app_purchase_platform.dart 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/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md index 223d0441a7..4b7329ca44 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/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 10ce08d63f..73be3fb5f4 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 @@ -9,7 +9,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.billingclient.api.BillingClient; import com.android.billingclient.api.UserChoiceBillingListener; -import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.inapppurchase.Messages.PlatformBillingChoiceMode; /** Responsible for creating a {@link BillingClient} object. */ interface BillingClientFactory { @@ -18,14 +18,14 @@ 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, - int billingChoiceMode, + @NonNull Messages.InAppPurchaseCallbackApi callbackApi, + PlatformBillingChoiceMode billingChoiceMode, @Nullable UserChoiceBillingListener userChoiceBillingListener); } 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 4c53951a49..f7bece9612 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 @@ -10,8 +10,7 @@ import androidx.annotation.Nullable; import com.android.billingclient.api.BillingClient; import com.android.billingclient.api.UserChoiceBillingListener; import io.flutter.Log; -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 { @@ -19,16 +18,16 @@ final class BillingClientFactoryImpl implements BillingClientFactory { @Override public BillingClient createBillingClient( @NonNull Context context, - @NonNull MethodChannel channel, - int billingChoiceMode, + @NonNull Messages.InAppPurchaseCallbackApi callbackApi, + PlatformBillingChoiceMode billingChoiceMode, @Nullable UserChoiceBillingListener userChoiceBillingListener) { BillingClient.Builder builder = BillingClient.newBuilder(context).enablePendingPurchases(); switch (billingChoiceMode) { - case BillingChoiceMode.ALTERNATIVE_BILLING_ONLY: + case ALTERNATIVE_BILLING_ONLY: // https://developer.android.com/google/play/billing/alternative/alternative-billing-without-user-choice-in-app builder.enableAlternativeBillingOnly(); break; - case BillingChoiceMode.USER_CHOICE_BILLING: + case USER_CHOICE_BILLING: if (userChoiceBillingListener != null) { // https://developer.android.com/google/play/billing/alternative/alternative-billing-with-user-choice-in-app builder.enableUserChoiceBilling(userChoiceBillingListener); @@ -38,7 +37,7 @@ final class BillingClientFactoryImpl implements BillingClientFactory { "userChoiceBillingListener null when USER_CHOICE_BILLING set. Defaulting to PLAY_BILLING_ONLY"); } break; - case BillingChoiceMode.PLAY_BILLING_ONLY: + case PLAY_BILLING_ONLY: // Do nothing. break; default: @@ -47,6 +46,6 @@ final class BillingClientFactoryImpl implements BillingClientFactory { "Unknown BillingChoiceMode " + billingChoiceMode + ", Defaulting to PLAY_BILLING_ONLY"); break; } - 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 4db2ca5d79..cc153dc430 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.FlutterPlugin; 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. */ @@ -45,7 +43,7 @@ public class InAppPurchasePlugin implements FlutterPlugin, ActivityAware { @Override public void onDetachedFromEngine(@NonNull FlutterPlugin.FlutterPluginBinding binding) { - teardownMethodChannel(); + teardownMethodChannel(binding.getBinaryMessenger()); } @Override @@ -71,16 +69,15 @@ public class InAppPurchasePlugin implements FlutterPlugin, ActivityAware { } 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()); - methodChannel.setMethodCallHandler(methodCallHandler); + /*activity=*/ null, context, handler, new BillingClientFactoryImpl()); + Messages.InAppPurchaseApi.setUp(messenger, methodCallHandler); } - private void teardownMethodChannel() { - methodChannel.setMethodCallHandler(null); - methodChannel = null; + private void teardownMethodChannel(BinaryMessenger messenger) { + Messages.InAppPurchaseApi.setUp(messenger, 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 0000000000..09701660aa --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -0,0 +1,1690 @@ +// 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.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +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; +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.io.ByteArrayOutputStream; +import java.lang.annotation.Retention; +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. */ +@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; + } + + @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 {} + + /** 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 { + /** + * Billing through google play. + * + *

Default state. + */ + PLAY_BILLING_ONLY(0), + /** Billing through app provided flow. */ + ALTERNATIVE_BILLING_ONLY(1), + /** Users can choose Play billing or alternative billing. */ + USER_CHOICE_BILLING(2); + + final int index; + + private PlatformBillingChoiceMode(final int index) { + this.index = 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. + * + *

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; + } + } + + /** + * 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 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 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. + * + *

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; + } + } + + /** + * 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. + * + *

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; + } + } + + /** + * Pigeon version of UserChoiceDetailsWrapper and Java UserChoiceDetails. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PlatformUserChoiceDetails { + private @Nullable String originalExternalTransactionId; + + public @Nullable String getOriginalExternalTransactionId() { + return originalExternalTransactionId; + } + + public void setOriginalExternalTransactionId(@Nullable String setterArg) { + this.originalExternalTransactionId = 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; + } + + /** + * A JSON-compatible list of products, where each entry in the list is a Map + * JSON encoding of the product. + */ + private @NonNull List productsJsonList; + + public @NonNull List getProductsJsonList() { + return productsJsonList; + } + + public void setProductsJsonList(@NonNull List setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"productsJsonList\" is null."); + } + this.productsJsonList = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PlatformUserChoiceDetails() {} + + public static final class Builder { + + private @Nullable String originalExternalTransactionId; + + @CanIgnoreReturnValue + public @NonNull Builder setOriginalExternalTransactionId(@Nullable String setterArg) { + this.originalExternalTransactionId = setterArg; + return this; + } + + private @Nullable String externalTransactionToken; + + @CanIgnoreReturnValue + public @NonNull Builder setExternalTransactionToken(@NonNull String setterArg) { + this.externalTransactionToken = setterArg; + return this; + } + + private @Nullable List productsJsonList; + + @CanIgnoreReturnValue + public @NonNull Builder setProductsJsonList(@NonNull List setterArg) { + this.productsJsonList = setterArg; + return this; + } + + public @NonNull PlatformUserChoiceDetails build() { + PlatformUserChoiceDetails pigeonReturn = new PlatformUserChoiceDetails(); + pigeonReturn.setOriginalExternalTransactionId(originalExternalTransactionId); + pigeonReturn.setExternalTransactionToken(externalTransactionToken); + pigeonReturn.setProductsJsonList(productsJsonList); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(3); + toListResult.add(originalExternalTransactionId); + toListResult.add(externalTransactionToken); + toListResult.add(productsJsonList); + return toListResult; + } + + static @NonNull PlatformUserChoiceDetails fromList(@NonNull ArrayList list) { + PlatformUserChoiceDetails pigeonResult = new PlatformUserChoiceDetails(); + Object originalExternalTransactionId = list.get(0); + pigeonResult.setOriginalExternalTransactionId((String) originalExternalTransactionId); + Object externalTransactionToken = list.get(1); + pigeonResult.setExternalTransactionToken((String) externalTransactionToken); + Object productsJsonList = list.get(2); + pigeonResult.setProductsJsonList((List) productsJsonList); + return pigeonResult; + } + } + + /** 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); + } + + 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 PlatformAlternativeBillingOnlyReportingDetailsResponse.fromList( + (ArrayList) readValue(buffer)); + case (byte) 129: + return PlatformBillingConfigResponse.fromList((ArrayList) readValue(buffer)); + case (byte) 130: + return PlatformBillingFlowParams.fromList((ArrayList) readValue(buffer)); + case (byte) 131: + return PlatformBillingResult.fromList((ArrayList) readValue(buffer)); + case (byte) 132: + return PlatformProduct.fromList((ArrayList) readValue(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); + } + } + + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof PlatformAlternativeBillingOnlyReportingDetailsResponse) { + stream.write(128); + writeValue( + stream, ((PlatformAlternativeBillingOnlyReportingDetailsResponse) value).toList()); + } 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(131); + writeValue(stream, ((PlatformBillingResult) value).toList()); + } else if (value instanceof PlatformProduct) { + stream.write(132); + writeValue(stream, ((PlatformProduct) value).toList()); + } else if (value instanceof PlatformProductDetailsResponse) { + stream.write(133); + writeValue(stream, ((PlatformProductDetailsResponse) value).toList()); + } 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); + } + } + } + + /** 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); + /** 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); + /** + * 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#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener). */ + 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). + */ + void queryProductDetailsAsync( + @NonNull List products, + @NonNull Result result); + /** Wraps BillingClient#isFeatureSupported(String). */ + @NonNull + Boolean isFeatureSupported(@NonNull String feature); + /** Wraps BillingClient#isAlternativeBillingOnlyAvailableAsync(). */ + 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() { + return InAppPurchaseApiCodec.INSTANCE; + } + /** + * 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); + } + } + { + 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(PlatformBillingResult 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); + } + } + { + 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); + } + } + { + 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<>( + 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<>( + 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<>( + 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<>( + 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<>( + 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<>( + 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<>( + 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<>( + binaryMessenger, + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.isAlternativeBillingOnlyAvailableAsync", + 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.isAlternativeBillingOnlyAvailableAsync(resultCallback); + }); + } else { + 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); + } + } + { + 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); + } + } + } + } + + 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)); + case (byte) 130: + return PlatformUserChoiceDetails.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 if (value instanceof PlatformUserChoiceDetails) { + stream.write(130); + writeValue(stream, ((PlatformUserChoiceDetails) 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)); + } + }); + } + /** Called for UserChoiceBillingListener#userSelectedAlternativeBilling(UserChoiceDetails). */ + public void userSelectedalternativeBilling( + @NonNull PlatformUserChoiceDetails detailsArg, @NonNull VoidResult result) { + final String channelName = + "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseCallbackApi.userSelectedalternativeBilling"; + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, channelName, getCodec()); + channel.send( + new ArrayList(Collections.singletonList(detailsArg)), + 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 0343a8a9d4..c9543f1328 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.fromPurchaseHistoryRec import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; import static io.flutter.plugins.inapppurchase.Translator.fromUserChoiceDetails; import static io.flutter.plugins.inapppurchase.Translator.toProductList; +import static io.flutter.plugins.inapppurchase.Translator.toProductTypeString; import android.app.Activity; import android.app.Application; @@ -31,81 +32,33 @@ import com.android.billingclient.api.ConsumeResponseListener; 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 com.android.billingclient.api.UserChoiceBillingListener; -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.InAppPurchaseCallbackApi; +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 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 { - - @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()"; - 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 = - "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)"; - 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 = - "BillingClient#showAlternativeBillingOnlyInformationDialog()"; - static final String USER_SELECTED_ALTERNATIVE_BILLING = - "UserChoiceBillingListener#userSelectedAlternativeBilling(UserChoiceDetails)"; - - 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; - static final int USER_CHOICE_BILLING = 2; - } - +class MethodCallHandlerImpl implements Application.ActivityLifecycleCallbacks, InAppPurchaseApi { // TODO(gmackall): Replace uses of deprecated ProrationMode enum values with new // 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; @@ -119,7 +72,7 @@ class MethodCallHandlerImpl @Nullable private Activity activity; private final Context applicationContext; - final MethodChannel methodChannel; + final InAppPurchaseCallbackApi callbackApi; private final HashMap cachedProducts = new HashMap<>(); @@ -127,12 +80,12 @@ class MethodCallHandlerImpl 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; } /** @@ -144,22 +97,22 @@ class MethodCallHandlerImpl } @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(); @@ -167,132 +120,84 @@ class MethodCallHandlerImpl } @Override - public void onActivityStopped(Activity activity) {} + public void onActivityStopped(@NonNull Activity activity) {} void onDetachedFromActivity() { endBillingClientConnection(); } @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; - 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; - case MethodNames.QUERY_PRODUCT_DETAILS: - 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; - 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; - 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; - case MethodNames.IS_ALTERNATIVE_BILLING_ONLY_AVAILABLE: - isAlternativeBillingOnlyAvailable(result); - break; - 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)) { + public void showAlternativeBillingOnlyInformationDialog( + @NonNull Result result) { + if (billingClient == null) { + result.error(getNullBillingClientError()); return; } if (activity == null) { - result.error(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(fromBillingResult(billingResult)); - }); + try { + billingClient.showAlternativeBillingOnlyInformationDialog( + activity, billingResult -> result.success(fromBillingResult(billingResult))); + } catch (RuntimeException e) { + result.error(new FlutterError("error", e.getMessage(), Log.getStackTraceString(e))); + } } - private void createAlternativeBillingOnlyReportingDetails(final MethodChannel.Result result) { - if (billingClientError(result)) { + @Override + public void createAlternativeBillingOnlyReportingDetailsAsync( + @NonNull Result result) { + if (billingClient == null) { + 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))); + } } - private void isAlternativeBillingOnlyAvailable(final MethodChannel.Result result) { - if (billingClientError(result)) { + @Override + public void isAlternativeBillingOnlyAvailableAsync( + @NonNull Result result) { + if (billingClient == null) { + 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))); + } } - private void getBillingConfig(final MethodChannel.Result result) { - if (billingClientError(result)) { + @Override + public void getBillingConfigAsync( + @NonNull Result result) { + if (billingClient == null) { + 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))); + } } - private void endConnection(final MethodChannel.Result result) { + @Override + public void endConnection() { endBillingClientConnection(); - result.success(null); } private void endBillingClientConnection() { @@ -302,57 +207,59 @@ class MethodCallHandlerImpl } } - private void isReady(MethodChannel.Result result) { - if (billingClientError(result)) { - return; + @Override + @NonNull + public Boolean isReady() { + if (billingClient == null) { + throw getNullBillingClientError(); } - - result.success(billingClient.isReady()); + return billingClient.isReady(); } - private void queryProductDetailsAsync( - final List productList, final MethodChannel.Result result) { - if (billingClientError(result)) { + @Override + public void queryProductDetailsAsync( + @NonNull List products, + @NonNull Result result) { + if (billingClient == null) { + result.error(getNullBillingClientError()); return; } - QueryProductDetailsParams params = - QueryProductDetailsParams.newBuilder().setProductList(productList).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); - }); + 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))); + } } - 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 PlatformBillingResult launchBillingFlow( + @NonNull PlatformBillingFlowParams params) { + if (billingClient == null) { + throw getNullBillingClientError(); } - 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 @@ -361,58 +268,57 @@ class MethodCallHandlerImpl 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<>(); @@ -420,22 +326,24 @@ class MethodCallHandlerImpl 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 fromBillingResult(billingClient.launchBillingFlow(activity, paramsBuilder.build())); } // TODO(gmackall): Replace uses of deprecated setReplaceProrationMode. @@ -448,122 +356,171 @@ class MethodCallHandlerImpl builder.setReplaceProrationMode(prorationMode); } - private void consumeAsync(String purchaseToken, final MethodChannel.Result result) { - if (billingClientError(result)) { - return; - } - - ConsumeResponseListener listener = - (billingResult, outToken) -> result.success(fromBillingResult(billingResult)); - ConsumeParams.Builder paramsBuilder = - ConsumeParams.newBuilder().setPurchaseToken(purchaseToken); - - ConsumeParams params = paramsBuilder.build(); - - billingClient.consumeAsync(params, listener); - } - - private void queryPurchasesAsync(String productType, MethodChannel.Result result) { - if (billingClientError(result)) { - 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(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); - }); - } - - private void queryPurchaseHistoryAsync(String productType, final MethodChannel.Result result) { - if (billingClientError(result)) { - return; - } - - billingClient.queryPurchaseHistoryAsync( - QueryPurchaseHistoryParams.newBuilder().setProductType(productType).build(), - (billingResult, purchasesList) -> { - final Map serialized = new HashMap<>(); - serialized.put("billingResult", fromBillingResult(billingResult)); - serialized.put("purchaseHistoryRecordList", fromPurchaseHistoryRecordList(purchasesList)); - result.success(serialized); - }); - } - - private void getConnectionState(final MethodChannel.Result result) { - if (billingClientError(result)) { - return; - } - final Map serialized = new HashMap<>(); - serialized.put("connectionState", billingClient.getConnectionState()); - result.success(serialized); - } - - private void startConnection( - final int handle, final MethodChannel.Result result, int billingChoiceMode) { + @Override + public void consumeAsync( + @NonNull String purchaseToken, @NonNull Result result) { if (billingClient == null) { - UserChoiceBillingListener listener = getUserChoiceBillingListener(billingChoiceMode); + result.error(getNullBillingClientError()); + return; + } + + try { + ConsumeResponseListener listener = + (billingResult, outToken) -> result.success(fromBillingResult(billingResult)); + ConsumeParams.Builder paramsBuilder = + ConsumeParams.newBuilder().setPurchaseToken(purchaseToken); + ConsumeParams params = paramsBuilder.build(); + + billingClient.consumeAsync(params, listener); + } catch (RuntimeException e) { + result.error(new FlutterError("error", e.getMessage(), Log.getStackTraceString(e))); + } + } + + @Override + public void queryPurchasesAsync( + @NonNull PlatformProductType productType, + @NonNull Result result) { + if (billingClient == null) { + result.error(getNullBillingClientError()); + return; + } + + 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 + public void queryPurchaseHistoryAsync( + @NonNull PlatformProductType productType, + @NonNull Result result) { + if (billingClient == null) { + result.error(getNullBillingClientError()); + return; + } + + 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 + public void startConnection( + @NonNull Long handle, + @NonNull PlatformBillingChoiceMode billingMode, + @NonNull Result result) { + if (billingClient == null) { + UserChoiceBillingListener listener = getUserChoiceBillingListener(billingMode); billingClient = billingClientFactory.createBillingClient( - applicationContext, methodChannel, billingChoiceMode, listener); + 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; + @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)); } - 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() { - final Map arguments = new HashMap<>(); - arguments.put("handle", handle); - methodChannel.invokeMethod(MethodNames.ON_DISCONNECT, arguments); - } - }); + @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 - private UserChoiceBillingListener getUserChoiceBillingListener(int billingChoiceMode) { + private UserChoiceBillingListener getUserChoiceBillingListener( + PlatformBillingChoiceMode billingChoiceMode) { UserChoiceBillingListener listener = null; - if (billingChoiceMode == BillingChoiceMode.USER_CHOICE_BILLING) { + if (billingChoiceMode == PlatformBillingChoiceMode.USER_CHOICE_BILLING) { listener = - userChoiceDetails -> { - final Map arguments = fromUserChoiceDetails(userChoiceDetails); - methodChannel.invokeMethod(MethodNames.USER_SELECTED_ALTERNATIVE_BILLING, arguments); - }; + userChoiceDetails -> + callbackApi.userSelectedalternativeBilling( + fromUserChoiceDetails(userChoiceDetails), + new Messages.VoidResult() { + @Override + public void success() {} + + @Override + public void error(@NonNull Throwable error) { + io.flutter.Log.e( + "IN_APP_PURCHASE", + "userSelectedalternativeBilling handler error: " + error); + } + }); } return listener; } - private void acknowledgePurchase(String purchaseToken, final MethodChannel.Result result) { - if (billingClientError(result)) { + @Override + public void acknowledgePurchase( + @NonNull String purchaseToken, @NonNull Result result) { + if (billingClient == null) { + 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) { @@ -576,21 +533,16 @@ class MethodCallHandlerImpl } } - 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); } - private void isFeatureSupported(String feature, MethodChannel.Result result) { - if (billingClientError(result)) { - return; + @Override + public @NonNull Boolean isFeatureSupported(@NonNull String feature) { + if (billingClient == null) { + throw getNullBillingClientError(); } - 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/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 ce919f75d1..e8f1b40aeb 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 @@ -9,33 +9,36 @@ import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; 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", fromBillingResult(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 f9e91659bc..195cc7335a 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.NonNull; 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; @@ -22,7 +23,6 @@ import java.util.Currency; 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} @@ -58,32 +58,41 @@ import java.util.Map; 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)); } @@ -203,51 +212,59 @@ import java.util.Map; 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)); } 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)); } return serialized; } - static HashMap fromBillingResult(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()) + .setDebugMessage(billingResult.getDebugMessage()) + .build(); } - static HashMap fromUserChoiceDetails(UserChoiceDetails userChoiceDetails) { - HashMap info = new HashMap<>(); - info.put("externalTransactionToken", userChoiceDetails.getExternalTransactionToken()); - info.put("originalExternalTransactionId", userChoiceDetails.getOriginalExternalTransactionId()); - info.put("products", fromProductsList(userChoiceDetails.getProducts())); - return info; + static Messages.PlatformUserChoiceDetails fromUserChoiceDetails( + UserChoiceDetails userChoiceDetails) { + return new Messages.PlatformUserChoiceDetails.Builder() + .setExternalTransactionToken(userChoiceDetails.getExternalTransactionToken()) + .setOriginalExternalTransactionId(userChoiceDetails.getOriginalExternalTransactionId()) + .setProductsJsonList(fromProductsList(userChoiceDetails.getProducts())) + .build(); } - static List> fromProductsList(List productsList) { + static List fromProductsList(List productsList) { if (productsList.isEmpty()) { 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 (Product product : productsList) { output.add(fromProduct(product)); } @@ -264,23 +281,24 @@ import java.util.Map; } /** Converter from {@link BillingResult} and {@link BillingConfig} to map. */ - static HashMap fromBillingConfig( + static Messages.PlatformBillingConfigResponse fromBillingConfig( BillingResult result, BillingConfig billingConfig) { - HashMap info = fromBillingResult(result); - info.put("countryCode", billingConfig.getCountryCode()); - return info; + return new Messages.PlatformBillingConfigResponse.Builder() + .setBillingResult(fromBillingResult(result)) + .setCountryCode(billingConfig.getCountryCode()) + .build(); } /** * 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 + fromAlternativeBillingOnlyReportingDetails( + BillingResult result, AlternativeBillingOnlyReportingDetails details) { + return new Messages.PlatformAlternativeBillingOnlyReportingDetailsResponse.Builder() + .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 42acbf5f86..e3fdb6b4df 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,38 +5,20 @@ 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.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; -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; -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.MethodCallHandlerImpl.MethodNames.USER_SELECTED_ALTERNATIVE_BILLING; -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.MethodCallHandlerImpl.PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY; 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.fromUserChoiceDetails; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; 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; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.refEq; import static org.mockito.Mockito.doAnswer; @@ -49,7 +31,6 @@ import static org.mockito.Mockito.when; 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; @@ -78,24 +59,29 @@ import com.android.billingclient.api.QueryPurchasesParams; import com.android.billingclient.api.UserChoiceBillingListener; import com.android.billingclient.api.UserChoiceDetails; 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.MethodCallHandlerImpl.BillingChoiceMode; -import io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodArgs; +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; +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 io.flutter.plugins.inapppurchase.Messages.PlatformPurchaseHistoryResponse; +import io.flutter.plugins.inapppurchase.Messages.PlatformPurchasesResponse; +import io.flutter.plugins.inapppurchase.Messages.PlatformUserChoiceDetails; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; +import java.util.Objects; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; -import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @@ -103,150 +89,129 @@ import org.mockito.Spy; import org.mockito.stubbing.Answer; public class MethodCallHandlerTest { + private AutoCloseable openMocks; private MethodCallHandlerImpl methodChannelHandler; @Mock BillingClientFactory factory; @Mock BillingClient mockBillingClient; - @Mock MethodChannel mockMethodChannel; - @Spy Result result; + @Mock InAppPurchaseCallbackApi mockCallbackApi; + + @Spy + Messages.Result + platformAlternativeBillingOnlyReportingDetailsResult; + + @Spy Messages.Result platformBillingConfigResult; + @Spy Messages.Result platformBillingResult; + @Spy Messages.Result platformProductDetailsResult; + @Spy Messages.Result platformPurchaseHistoryResult; + @Spy Messages.Result platformPurchasesResult; + @Mock Activity activity; @Mock Context context; @Mock ActivityPluginBinding mockActivityPluginBinding; - @Captor ArgumentCaptor> resultCaptor; - private final int DEFAULT_HANDLE = 1; + private final Long DEFAULT_HANDLE = 1L; @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, BillingChoiceMode.PLAY_BILLING_ONLY, null)) + context, mockCallbackApi, PlatformBillingChoiceMode.PLAY_BILLING_ONLY, null)) .thenReturn(mockBillingClient); when(factory.createBillingClient( - context, mockMethodChannel, BillingChoiceMode.ALTERNATIVE_BILLING_ONLY, null)) + context, mockCallbackApi, PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY, null)) .thenReturn(mockBillingClient); when(factory.createBillingClient( any(Context.class), - any(MethodChannel.class), - eq(BillingChoiceMode.USER_CHOICE_BILLING), + any(InAppPurchaseCallbackApi.class), + eq(PlatformBillingChoiceMode.USER_CHOICE_BILLING), any(UserChoiceBillingListener.class))) .thenReturn(mockBillingClient); - methodChannelHandler = new MethodCallHandlerImpl(activity, context, mockMethodChannel, factory); + methodChannelHandler = new MethodCallHandlerImpl(activity, context, mockCallbackApi, factory); when(mockActivityPluginBinding.getActivity()).thenReturn(activity); } - @Test - public void invalidMethod() { - MethodCall call = new MethodCall("invalid", null); - methodChannelHandler.onMethodCall(call, result); - verify(result, times(1)).notImplemented(); + @After + public void tearDown() throws Exception { + openMocks.close(); } @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.endConnection(); - methodChannelHandler.onMethodCall(isReadyCall, result); - - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); - verify(result, never()).success(any()); + // 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")); } @Test public void startConnection() { ArgumentCaptor captor = - mockStartConnection(BillingChoiceMode.PLAY_BILLING_ONLY); - verify(result, never()).success(any()); + mockStartConnection(PlatformBillingChoiceMode.PLAY_BILLING_ONLY); + verify(platformBillingResult, never()).success(any()); verify(factory, times(1)) - .createBillingClient(context, mockMethodChannel, BillingChoiceMode.PLAY_BILLING_ONLY, null); + .createBillingClient( + context, mockCallbackApi, PlatformBillingChoiceMode.PLAY_BILLING_ONLY, null); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); captor.getValue().onBillingSetupFinished(billingResult); - verify(result, times(1)).success(fromBillingResult(billingResult)); + ArgumentCaptor resultCaptor = + ArgumentCaptor.forClass(PlatformBillingResult.class); + verify(platformBillingResult, times(1)).success(resultCaptor.capture()); + assertResultsMatch(resultCaptor.getValue(), billingResult); + verify(platformBillingResult, never()).error(any()); } @Test public void startConnectionAlternativeBillingOnly() { ArgumentCaptor captor = - mockStartConnection(BillingChoiceMode.ALTERNATIVE_BILLING_ONLY); - verify(result, never()).success(any()); + mockStartConnection(PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY); + verify(platformBillingResult, never()).success(any()); verify(factory, times(1)) .createBillingClient( - context, mockMethodChannel, BillingChoiceMode.ALTERNATIVE_BILLING_ONLY, null); + context, mockCallbackApi, PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY, null); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); 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, null); - - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); - captor.getValue().onBillingSetupFinished(billingResult); - - verify(result, times(1)).success(fromBillingResult(billingResult)); + ArgumentCaptor resultCaptor = + ArgumentCaptor.forClass(PlatformBillingResult.class); + verify(platformBillingResult, times(1)).success(resultCaptor.capture()); + assertResultsMatch(resultCaptor.getValue(), billingResult); + verify(platformBillingResult, never()).error(any()); } @Test public void startConnectionUserChoiceBilling() { ArgumentCaptor captor = - mockStartConnection(BillingChoiceMode.USER_CHOICE_BILLING); + mockStartConnection(PlatformBillingChoiceMode.USER_CHOICE_BILLING); ArgumentCaptor billingCaptor = ArgumentCaptor.forClass(UserChoiceBillingListener.class); - verify(result, never()).success(any()); + verify(platformBillingResult, never()).success(any()); verify(factory, times(1)) .createBillingClient( any(Context.class), - any(MethodChannel.class), - eq(BillingChoiceMode.USER_CHOICE_BILLING), + any(InAppPurchaseCallbackApi.class), + eq(PlatformBillingChoiceMode.USER_CHOICE_BILLING), billingCaptor.capture()); BillingResult billingResult = @@ -256,7 +221,9 @@ public class MethodCallHandlerTest { .build(); captor.getValue().onBillingSetupFinished(billingResult); - verify(result, times(1)).success(fromBillingResult(billingResult)); + ArgumentCaptor resultCaptor = + ArgumentCaptor.forClass(PlatformBillingResult.class); + verify(platformBillingResult, times(1)).success(resultCaptor.capture()); UserChoiceDetails details = mock(UserChoiceDetails.class); final String externalTransactionToken = "someLongTokenId1234"; final String originalTransactionId = "originalTransactionId123456"; @@ -265,18 +232,25 @@ public class MethodCallHandlerTest { when(details.getProducts()).thenReturn(Collections.emptyList()); billingCaptor.getValue().userSelectedAlternativeBilling(details); - verify(mockMethodChannel, times(1)) - .invokeMethod(USER_SELECTED_ALTERNATIVE_BILLING, fromUserChoiceDetails(details)); + ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(PlatformUserChoiceDetails.class); + verify(mockCallbackApi, times(1)) + .userSelectedalternativeBilling(callbackCaptor.capture(), any()); + assertEquals(callbackCaptor.getValue().getExternalTransactionToken(), externalTransactionToken); + assertEquals( + callbackCaptor.getValue().getOriginalExternalTransactionId(), originalTransactionId); + assertTrue(callbackCaptor.getValue().getProductsJsonList().isEmpty()); } @Test public void userChoiceBillingOnSecondConnection() { // First connection. ArgumentCaptor captor1 = - mockStartConnection(BillingChoiceMode.PLAY_BILLING_ONLY); - verify(result, never()).success(any()); + mockStartConnection(PlatformBillingChoiceMode.PLAY_BILLING_ONLY); + verify(platformBillingResult, never()).success(any()); verify(factory, times(1)) - .createBillingClient(context, mockMethodChannel, BillingChoiceMode.PLAY_BILLING_ONLY, null); + .createBillingClient( + context, mockCallbackApi, PlatformBillingChoiceMode.PLAY_BILLING_ONLY, null); BillingResult billingResult1 = BillingResult.newBuilder() @@ -285,34 +259,30 @@ public class MethodCallHandlerTest { .build(); final BillingClientStateListener stateListener = captor1.getValue(); stateListener.onBillingSetupFinished(billingResult1); - verify(result, times(1)).success(fromBillingResult(billingResult1)); - Mockito.reset(result, mockMethodChannel, mockBillingClient); + verify(platformBillingResult, times(1)).success(any()); + Mockito.reset(platformBillingResult, mockCallbackApi, mockBillingClient); // Disconnect - 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<>(); - expectedInvocation.put("handle", DEFAULT_HANDLE); - verify(mockMethodChannel, times(1)).invokeMethod(ON_DISCONNECT, expectedInvocation); - Mockito.reset(result, mockMethodChannel, mockBillingClient); + verify(mockCallbackApi, times(1)).onBillingServiceDisconnected(eq(DEFAULT_HANDLE), any()); + Mockito.reset(platformBillingResult, mockCallbackApi, mockBillingClient); // Second connection. ArgumentCaptor captor2 = - mockStartConnection(BillingChoiceMode.USER_CHOICE_BILLING); + mockStartConnection(PlatformBillingChoiceMode.USER_CHOICE_BILLING); ArgumentCaptor billingCaptor = ArgumentCaptor.forClass(UserChoiceBillingListener.class); - verify(result, never()).success(any()); + verify(platformBillingResult, never()).success(any()); verify(factory, times(1)) .createBillingClient( any(Context.class), - any(MethodChannel.class), - eq(BillingChoiceMode.USER_CHOICE_BILLING), + any(InAppPurchaseCallbackApi.class), + eq(PlatformBillingChoiceMode.USER_CHOICE_BILLING), billingCaptor.capture()); BillingResult billingResult2 = @@ -322,7 +292,7 @@ public class MethodCallHandlerTest { .build(); captor2.getValue().onBillingSetupFinished(billingResult2); - verify(result, times(1)).success(fromBillingResult(billingResult2)); + verify(platformBillingResult, times(1)).success(any()); UserChoiceDetails details = mock(UserChoiceDetails.class); final String externalTransactionToken = "someLongTokenId1234"; final String originalTransactionId = "originalTransactionId123456"; @@ -331,26 +301,26 @@ public class MethodCallHandlerTest { when(details.getProducts()).thenReturn(Collections.emptyList()); billingCaptor.getValue().userSelectedAlternativeBilling(details); - verify(mockMethodChannel, times(1)) - .invokeMethod(USER_SELECTED_ALTERNATIVE_BILLING, fromUserChoiceDetails(details)); + ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(PlatformUserChoiceDetails.class); + verify(mockCallbackApi, times(1)) + .userSelectedalternativeBilling(callbackCaptor.capture(), any()); + assertEquals(callbackCaptor.getValue().getExternalTransactionToken(), externalTransactionToken); + assertEquals( + callbackCaptor.getValue().getOriginalExternalTransactionId(), originalTransactionId); + assertTrue(callbackCaptor.getValue().getProductsJsonList().isEmpty()); } @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()); - BillingResult billingResult1 = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + methodChannelHandler.startConnection( + DEFAULT_HANDLE, PlatformBillingChoiceMode.PLAY_BILLING_ONLY, platformBillingResult); + verify(platformBillingResult, never()).success(any()); + BillingResult billingResult1 = buildBillingResult(); BillingResult billingResult2 = BillingResult.newBuilder() .setResponseCode(200) @@ -366,8 +336,13 @@ public class MethodCallHandlerTest { captor.getValue().onBillingSetupFinished(billingResult2); captor.getValue().onBillingSetupFinished(billingResult3); - verify(result, times(1)).success(fromBillingResult(billingResult1)); - verify(result, times(1)).success(any()); + ArgumentCaptor resultCaptor = + ArgumentCaptor.forClass(PlatformBillingResult.class); + verify(platformBillingResult, times(1)).success(resultCaptor.capture()); + assertEquals( + resultCaptor.getValue().getResponseCode().longValue(), billingResult1.getResponseCode()); + assertEquals(resultCaptor.getValue().getDebugMessage(), billingResult1.getDebugMessage()); + verify(platformBillingResult, never()).error(any()); } @Test @@ -377,12 +352,7 @@ public class MethodCallHandlerTest { 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); @@ -391,20 +361,28 @@ public class MethodCallHandlerTest { .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); + verify(platformBillingConfigResult, never()).error(any()); } @Test public void getBillingConfig_serviceDisconnected() { - MethodCall billingCall = new MethodCall(GET_BILLING_CONFIG, null); - methodChannelHandler.onMethodCall(billingCall, mock(Result.class)); + methodChannelHandler.getBillingConfigAsync(platformBillingConfigResult); - methodChannelHandler.onMethodCall(billingCall, result); - - 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 @@ -412,13 +390,7 @@ public class MethodCallHandlerTest { 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) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(BillingResponseCode.OK); final AlternativeBillingOnlyReportingDetails expectedDetails = mock(AlternativeBillingOnlyReportingDetails.class); final String expectedExternalTransactionToken = "abc123youandme"; @@ -429,21 +401,33 @@ public class MethodCallHandlerTest { .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.createAlternativeBillingOnlyReportingDetailsAsync( + platformAlternativeBillingOnlyReportingDetailsResult); - methodChannelHandler.onMethodCall(createCall, result); - - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); + // 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 @@ -451,32 +435,33 @@ public class MethodCallHandlerTest { 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) - .setDebugMessage("dummy debug message") - .build(); - final HashMap expectedResult = fromBillingResult(billingResult); + BillingResult billingResult = buildBillingResult(BillingClient.BillingResponseCode.OK); doNothing() .when(mockBillingClient) .isAlternativeBillingOnlyAvailableAsync(listenerCaptor.capture()); - methodChannelHandler.onMethodCall(billingCall, result); + methodChannelHandler.isAlternativeBillingOnlyAvailableAsync(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()); + assertResultsMatch(resultCaptor.getValue(), billingResult); + verify(platformBillingResult, never()).error(any()); } @Test public void isAlternativeBillingOnlyAvailable_serviceDisconnected() { - MethodCall billingCall = new MethodCall(IS_ALTERNATIVE_BILLING_ONLY_AVAILABLE, null); - methodChannelHandler.onMethodCall(billingCall, mock(Result.class)); + methodChannelHandler.isAlternativeBillingOnlyAvailableAsync(platformBillingResult); - methodChannelHandler.onMethodCall(billingCall, result); - - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); + // 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 @@ -484,85 +469,87 @@ public class MethodCallHandlerTest { 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) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(BillingClient.BillingResponseCode.OK); when(mockBillingClient.showAlternativeBillingOnlyInformationDialog( 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()); + assertResultsMatch(resultCaptor.getValue(), billingResult); + verify(platformBillingResult, never()).error(any()); } @Test public void showAlternativeBillingOnlyInformationDialog_serviceDisconnected() { - MethodCall billingCall = new MethodCall(SHOW_ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG, null); + methodChannelHandler.showAlternativeBillingOnlyInformationDialog(platformBillingResult); - methodChannelHandler.onMethodCall(billingCall, result); - - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); + // 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 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()); + 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(errorCaptor.getValue().getMessage()) + .contains("Not attempting to show dialog")); } @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 - 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<>(); - 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 public void queryProductDetailsAsync() { // Connect a billing client and set up the product query listeners - establishConnectedBillingClient(/* arguments= */ null, /* result= */ null); - 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); + establishConnectedBillingClient(); + 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 = @@ -573,38 +560,35 @@ public class MethodCallHandlerTest { .queryProductDetailsAsync(paramCaptor.capture(), listenerCaptor.capture()); // Assert that we handed result BillingClient's response - int responseCode = 200; - List productDetailsResponse = asList(buildProductDetails("foo")); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + List productDetailsResponse = singletonList(buildProductDetails("foo")); + BillingResult billingResult = buildBillingResult(); 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 - MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); - methodChannelHandler.onMethodCall(disconnectCall, mock(Result.class)); - 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); + methodChannelHandler.endConnection(); + 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 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(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` @@ -615,30 +599,22 @@ public class MethodCallHandlerTest { // 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 = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); 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()); - 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)); + assertResultsMatch(platformResult, billingResult); } @Test @@ -647,29 +623,25 @@ public class MethodCallHandlerTest { 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 = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); 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()); - 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)); + + // Verify the response. + assertResultsMatch(platformResult, billingResult); } @Test @@ -680,15 +652,19 @@ public class MethodCallHandlerTest { 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(result, never()).success(any()); + // Assert that the synchronous call throws an exception. + FlutterError exception = + assertThrows( + FlutterError.class, + () -> methodChannelHandler.launchBillingFlow(paramsBuilder.build())); + assertEquals("ACTIVITY_UNAVAILABLE", exception.code); + assertTrue(Objects.requireNonNull(exception.getMessage()).contains("foreground")); } @Test @@ -698,30 +674,26 @@ public class MethodCallHandlerTest { 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 = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); 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()); - 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)); + // Verify the response. + assertResultsMatch(platformResult, billingResult); } @Test @@ -730,29 +702,25 @@ public class MethodCallHandlerTest { 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 = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); 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()); - 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)); + // Verify the response. + assertResultsMatch(platformResult, billingResult); } // TODO(gmackall): Replace uses of deprecated ProrationMode enum values with new @@ -768,32 +736,26 @@ public class MethodCallHandlerTest { 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 = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); 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()); - 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)); + // Verify the response. + assertResultsMatch(platformResult, billingResult); } // TODO(gmackall): Replace uses of deprecated ProrationMode enum values with new @@ -806,32 +768,27 @@ public class MethodCallHandlerTest { 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("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 = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); 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()); + // Assert that the synchronous call throws an exception. + FlutterError exception = + assertThrows( + 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 @@ -847,120 +804,112 @@ public class MethodCallHandlerTest { 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 = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); + BillingResult billingResult = buildBillingResult(); 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()); - 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)); + // Verify the response. + assertResultsMatch(platformResult, billingResult); } @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<>(); - 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); - methodChannelHandler.onMethodCall(launchCall, result); - - // Assert that we sent an error back. - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); - verify(result, never()).success(any()); + // Assert that the synchronous call throws an exception. + FlutterError exception = + assertThrows( + FlutterError.class, + () -> methodChannelHandler.launchBillingFlow(paramsBuilder.build())); + assertEquals("UNAVAILABLE", exception.code); + assertTrue(Objects.requireNonNull(exception.getMessage()).contains("BillingClient")); } @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<>(); - 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); - methodChannelHandler.onMethodCall(launchCall, result); - - // Assert that we sent an error back. - verify(result).error(contains("NOT_FOUND"), contains(productId), any()); - verify(result, never()).success(any()); + // Assert that the synchronous call throws an exception. + FlutterError exception = + assertThrows( + FlutterError.class, + () -> methodChannelHandler.launchBillingFlow(paramsBuilder.build())); + assertEquals("NOT_FOUND", exception.code); + assertTrue(Objects.requireNonNull(exception.getMessage()).contains(productId)); } @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"; 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); + 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); - methodChannelHandler.onMethodCall(launchCall, result); - - // 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()); + // Assert that the synchronous call throws an exception. + FlutterError exception = + assertThrows( + FlutterError.class, + () -> methodChannelHandler.launchBillingFlow(paramsBuilder.build())); + assertEquals("IN_APP_PURCHASE_INVALID_OLD_PRODUCT", exception.code); + assertTrue(Objects.requireNonNull(exception.getMessage()).contains(oldProductId)); } @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); - 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 - public void queryPurchases_returns_success() throws Exception { - establishConnectedBillingClient(null, null); - - CountDownLatch lock = new CountDownLatch(1); - doAnswer( - (Answer) - invocation -> { - lock.countDown(); - return null; - }) - .when(result) - .success(any(HashMap.class)); + public void queryPurchases_returns_success() { + establishConnectedBillingClient(); ArgumentCaptor purchasesResponseListenerArgumentCaptor = ArgumentCaptor.forClass(PurchasesResponseListener.class); @@ -973,153 +922,134 @@ public class MethodCallHandlerTest { .setDebugMessage("hello message"); purchasesResponseListenerArgumentCaptor .getValue() - .onQueryPurchasesResponse(resultBuilder.build(), new ArrayList()); + .onQueryPurchasesResponse(resultBuilder.build(), new ArrayList<>()); return null; }) .when(mockBillingClient) .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 public void queryPurchaseHistoryAsync() { // Set up an established billing client and all our mocked responses - establishConnectedBillingClient(null, null); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); - List purchasesList = asList(buildPurchaseHistoryRecord("foo")); - HashMap arguments = new HashMap<>(); - arguments.put("productType", BillingClient.ProductType.INAPP); + establishConnectedBillingClient(); + BillingResult billingResult = buildBillingResult(); + List purchasesList = singletonList(buildPurchaseHistoryRecord("foo")); 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() { - // 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); - 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 public void onPurchasesUpdatedListener() { - PluginPurchaseListener listener = new PluginPurchaseListener(mockMethodChannel); + PluginPurchaseListener listener = new PluginPurchaseListener(mockCallbackApi); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); - List purchasesList = asList(buildPurchase("foo")); - doNothing() - .when(mockMethodChannel) - .invokeMethod(eq(ON_PURCHASES_UPDATED), resultCaptor.capture()); + BillingResult billingResult = buildBillingResult(); + List purchasesList = singletonList(buildPurchase("foo")); + ArgumentCaptor resultCaptor = + ArgumentCaptor.forClass(PlatformPurchasesResponse.class); + doNothing().when(mockCallbackApi).onPurchasesUpdated(resultCaptor.capture(), any()); listener.onPurchasesUpdated(billingResult, purchasesList); - HashMap resultData = resultCaptor.getValue(); - assertEquals(fromBillingResult(billingResult), resultData.get("billingResult")); - assertEquals(fromPurchasesList(purchasesList), resultData.get("purchasesList")); + PlatformPurchasesResponse response = resultCaptor.getValue(); + assertResultsMatch(response.getBillingResult(), billingResult); + assertEquals(fromPurchasesList(purchasesList), response.getPurchasesJsonList()); } @Test public void consumeAsync() { - establishConnectedBillingClient(null, null); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); - HashMap arguments = new HashMap<>(); - arguments.put("purchaseToken", "mockToken"); - arguments.put("developerPayload", "mockPayload"); + establishConnectedBillingClient(); + BillingResult billingResult = buildBillingResult(); + 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 public void acknowledgePurchase() { - establishConnectedBillingClient(null, null); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); - HashMap arguments = new HashMap<>(); - arguments.put("purchaseToken", "mockToken"); - arguments.put("developerPayload", "mockPayload"); + establishConnectedBillingClient(); + BillingResult billingResult = buildBillingResult(); + 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 @@ -1135,92 +1065,64 @@ public class MethodCallHandlerTest { 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); + BillingResult billingResult = buildBillingResult(BillingClient.BillingResponseCode.OK); 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); + BillingResult billingResult = buildBillingResult(BillingResponseCode.FEATURE_NOT_SUPPORTED); when(mockBillingClient.isFeatureSupported(feature)).thenReturn(billingResult); - methodChannelHandler.onMethodCall(call, result); - verify(result).success(false); + + assertFalse(methodChannelHandler.isFeatureSupported(feature)); } /** - * 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, DEFAULT_HANDLE); - 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(DEFAULT_HANDLE, billingChoiceMode, platformBillingResult); 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( + DEFAULT_HANDLE, PlatformBillingChoiceMode.PLAY_BILLING_ONLY, mockResult); } private void queryForProducts(List productIdList) { // Set up the query method call - establishConnectedBillingClient(/* arguments= */ null, /* result= */ null); - 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); + establishConnectedBillingClient(); + 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 = @@ -1229,21 +1131,17 @@ public class MethodCallHandlerTest { 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); } - 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; } @@ -1282,4 +1180,20 @@ public class MethodCallHandlerTest { when(purchase.getPurchaseToken()).thenReturn(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/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 aa32afe2e4..d76f495894 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,10 +21,10 @@ import java.lang.reflect.Constructor; 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; +import java.util.Objects; import org.json.JSONException; import org.junit.Before; import org.junit.Test; @@ -78,7 +78,7 @@ public class TranslatorTest { 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)); @@ -123,8 +123,7 @@ public class TranslatorTest { 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)); @@ -145,7 +144,7 @@ public class TranslatorTest { 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)); @@ -164,20 +163,20 @@ public class TranslatorTest { .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 @@ -191,8 +190,9 @@ public class TranslatorTest { } } - 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")); @@ -275,7 +275,9 @@ public class TranslatorTest { 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")); @@ -286,7 +288,8 @@ public class TranslatorTest { 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")); @@ -296,7 +299,9 @@ public class TranslatorTest { 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_manager.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart index e9a3b50ce5..8e265dbdce 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'; @@ -17,6 +18,13 @@ 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, + UserSelectedAlternativeBillingListener? alternativeBillingListener); + /// Utility class that manages a [BillingClient] connection. /// /// Connection is initialized on creation of [BillingClientManager]. @@ -33,8 +41,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(); } @@ -57,8 +67,15 @@ class BillingClientManager { /// In order to access the [BillingClient], use [runWithClient] /// and [runWithClientNonRetryable] methods. @visibleForTesting - late final BillingClient client = - BillingClient(_onPurchasesUpdated, onUserChoiceAlternativeBilling); + late final BillingClient client = _billingClientFactory( + _onPurchasesUpdated, onUserChoiceAlternativeBilling); + + // Default (non-test) implementation of _billingClientFactory. + static BillingClient _createBillingClient( + PurchasesUpdatedListener onPurchasesUpdated, + UserSelectedAlternativeBillingListener? onUserChoiceAlternativeBilling) { + return BillingClient(onPurchasesUpdated, onUserChoiceAlternativeBilling); + } final StreamController _purchasesUpdatedController = StreamController.broadcast(); @@ -67,6 +84,7 @@ class BillingClientManager { StreamController.broadcast(); BillingChoiceMode _billingChoiceMode; + 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 b3684d8177..af6f4e8d09 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,28 +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)'; - -/// Method identifier for the userSelectedAlternativeBilling method channel method. -@visibleForTesting -const String kUserSelectedAlternativeBilling = - 'UserChoiceBillingListener#userSelectedAlternativeBilling(UserChoiceDetails)'; - -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 @@ -70,35 +57,28 @@ typedef UserSelectedAlternativeBillingListener = void Function( /// transparently. class BillingClient { /// Creates a billing client. - BillingClient(PurchasesUpdatedListener onPurchasesUpdated, - UserSelectedAlternativeBillingListener? alternativeBillingListener) { - channel.setMethodCallHandler(callHandler); - _callbacks[kOnPurchasesUpdated] = [ - onPurchasesUpdated - ]; - _callbacks[kUserSelectedAlternativeBilling] = alternativeBillingListener == - null - ? [] - : [alternativeBillingListener]; + BillingClient( + PurchasesUpdatedListener onPurchasesUpdated, + UserSelectedAlternativeBillingListener? alternativeBillingListener, { + @visibleForTesting InAppPurchaseApi? api, + }) : _hostApi = api ?? InAppPurchaseApi(), + hostCallbackHandler = HostBillingClientCallbackHandler( + onPurchasesUpdated, alternativeBillingListener) { + InAppPurchaseCallbackApi.setup(hostCallbackHandler); } - // 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 = >{}; + /// Interface for calling host-side code. + final InAppPurchaseApi _hostApi; + + /// 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()) /// 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. @@ -127,19 +107,10 @@ class BillingClient { {required OnBillingServiceDisconnected onBillingServiceDisconnected, BillingChoiceMode billingChoiceMode = BillingChoiceMode.playBillingOnly}) async { - final List disconnectCallbacks = - _callbacks[_kOnBillingServiceDisconnected] ??= []; - _callbacks[_kOnBillingServiceDisconnected] - ?.add(onBillingServiceDisconnected); - return BillingResultWrapper.fromJson((await channel - .invokeMapMethod( - 'BillingClient#startConnection(BillingClientStateListener)', - { - 'handle': disconnectCallbacks.length - 1, - 'billingChoiceMode': - const BillingChoiceModeConverter().toJson(billingChoiceMode), - })) ?? - {}); + hostCallbackHandler.disconnectCallbacks.add(onBillingServiceDisconnected); + return resultWrapperFromPlatform(await _hostApi.startConnection( + hostCallbackHandler.disconnectCallbacks.length - 1, + platformBillingChoiceMode(billingChoiceMode))); } /// Calls @@ -150,7 +121,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 @@ -166,16 +137,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]. @@ -227,21 +193,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]. @@ -256,14 +218,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 PurchasesResultWrapper.fromJson( - (await channel.invokeMapMethod( - 'BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)', - { - 'productType': const ProductTypeConverter().toJson(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]. @@ -280,13 +252,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. @@ -297,13 +265,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. @@ -325,119 +288,81 @@ 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. /// 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. - // - // 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()); } - /// 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.isAlternativeBillingOnlyAvailableAsync()); } - /// 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. - // - // 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()); + } +} + +/// 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, this.alternativeBillingListener); + + /// The handler for PurchasesUpdatedListener#onPurchasesUpdated. + final PurchasesUpdatedListener purchasesUpdatedCallback; + + /// The handler for UserChoiceBillingListener#userSelectedAlternativeBilling. + UserSelectedAlternativeBillingListener? alternativeBillingListener; + + /// Handlers for onBillingServiceDisconnected, indexed by handle identifier. + final List disconnectCallbacks = + []; + + @override + void onBillingServiceDisconnected(int callbackHandle) { + disconnectCallbacks[callbackHandle](); } - /// 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](); - case kUserSelectedAlternativeBilling: - if (_callbacks[kUserSelectedAlternativeBilling]!.isNotEmpty) { - final UserSelectedAlternativeBillingListener listener = - _callbacks[kUserSelectedAlternativeBilling]!.first - as UserSelectedAlternativeBillingListener; - listener(UserChoiceDetailsWrapper.fromJson( - (call.arguments as Map) - .cast())); - } - } + @override + void onPurchasesUpdated(PlatformPurchasesResponse update) { + purchasesUpdatedCallback(purchasesResultWrapperFromPlatform(update)); + } + + @override + void userSelectedalternativeBilling(PlatformUserChoiceDetails details) { + alternativeBillingListener!(userChoiceDetailsFromPlatform(details)); } } 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 62887b00d4..583dd4ef71 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/billing_client_wrappers/user_choice_details_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/user_choice_details_wrapper.dart index abdc31a178..4c4307e989 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/user_choice_details_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/user_choice_details_wrapper.dart @@ -19,7 +19,6 @@ part 'user_choice_details_wrapper.g.dart'; @immutable class UserChoiceDetailsWrapper { /// Creates a purchase wrapper with the given purchase details. - @visibleForTesting const UserChoiceDetailsWrapper({ required this.originalExternalTransactionId, required this.externalTransactionToken, 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 f8ab4d48be..0000000000 --- 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/in_app_purchase_android_platform.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart index 2e4ed5b0f9..9663aa414d 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,6 +4,7 @@ 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'; @@ -28,7 +29,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 +48,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 +62,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 0000000000..6f19a7b92a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -0,0 +1,971 @@ +// 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.2), 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".', + ); +} + +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, + subs, +} + +/// Pigeon version of billing_client_wrapper.dart's BillingChoiceMode. +enum PlatformBillingChoiceMode { + /// Billing through google play. + /// + /// Default state. + playBillingOnly, + + /// Billing through app provided flow. + alternativeBillingOnly, + + /// Users can choose Play billing or alternative billing. + userChoiceBilling, +} + +/// 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({ + 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, + ); + } +} + +/// 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 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 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({ + 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?, + ); + } +} + +/// 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 { + 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(), + ); + } +} + +/// Pigeon version of UserChoiceDetailsWrapper and Java UserChoiceDetails. +class PlatformUserChoiceDetails { + PlatformUserChoiceDetails({ + this.originalExternalTransactionId, + required this.externalTransactionToken, + required this.productsJsonList, + }); + + String? originalExternalTransactionId; + + String externalTransactionToken; + + /// A JSON-compatible list of products, where each entry in the list is a + /// Map JSON encoding of the product. + List productsJsonList; + + Object encode() { + return [ + originalExternalTransactionId, + externalTransactionToken, + productsJsonList, + ]; + } + + static PlatformUserChoiceDetails decode(Object result) { + result as List; + return PlatformUserChoiceDetails( + originalExternalTransactionId: result[0] as String?, + externalTransactionToken: result[1]! as String, + productsJsonList: (result[2] as List?)!.cast(), + ); + } +} + +class _InAppPurchaseApiCodec extends StandardMessageCodec { + const _InAppPurchaseApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is PlatformAlternativeBillingOnlyReportingDetailsResponse) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is PlatformBillingConfigResponse) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is PlatformBillingFlowParams) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is PlatformBillingResult) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } 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 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); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return PlatformAlternativeBillingOnlyReportingDetailsResponse.decode( + readValue(buffer)!); + case 129: + return PlatformBillingConfigResponse.decode(readValue(buffer)!); + case 130: + return PlatformBillingFlowParams.decode(readValue(buffer)!); + case 131: + return PlatformBillingResult.decode(readValue(buffer)!); + case 132: + return PlatformProduct.decode(readValue(buffer)!); + 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); + } + } +} + +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 = + _InAppPurchaseApiCodec(); + + /// 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?)!; + } + } + + /// Wraps BillingClient#startConnection(BillingClientStateListener). + 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 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; + } + } + + /// 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 { + 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 { + 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 = + '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#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#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 { + 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 = + '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 isAlternativeBillingOnlyAvailableAsync() async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.isAlternativeBillingOnlyAvailableAsync'; + 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?)!; + } + } + + /// 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?)!; + } + } + + /// 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?)!; + } + } +} + +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 if (value is PlatformUserChoiceDetails) { + buffer.putUint8(130); + 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)!); + case 130: + return PlatformUserChoiceDetails.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); + + /// Called for UserChoiceBillingListener#userSelectedAlternativeBilling(UserChoiceDetails). + void userSelectedalternativeBilling(PlatformUserChoiceDetails details); + + 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())); + } + }); + } + } + { + final BasicMessageChannel __pigeon_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseCallbackApi.userSelectedalternativeBilling', + 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.userSelectedalternativeBilling was null.'); + final List args = (message as List?)!; + final PlatformUserChoiceDetails? arg_details = + (args[0] as PlatformUserChoiceDetails?); + assert(arg_details != null, + 'Argument for dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseCallbackApi.userSelectedalternativeBilling was null, expected non-null PlatformUserChoiceDetails.'); + try { + api.userSelectedalternativeBilling(arg_details!); + 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 new file mode 100644 index 0000000000..232302f3a5 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart @@ -0,0 +1,125 @@ +// 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.dart'; +import 'billing_client_wrappers/billing_config_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, + BillingChoiceMode.userChoiceBilling => + PlatformBillingChoiceMode.userChoiceBilling, + }; +} + +/// 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 currently JSON. + productDetailsList: response.productDetailsJsonList + .map((Object? json) => ProductDetailsWrapper.fromJson( + (json! as Map).cast())) + .toList(), + ); +} + +/// 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, + {bool forceOkResponseCode = false}) { + 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(), + responseCode: forceOkResponseCode + ? BillingResponse.ok + : const BillingResponseConverter() + .fromJson(response.billingResult.responseCode), + ); +} + +/// Creates an [AlternativeBillingOnlyReportingDetailsWrapper] from the Pigeon +/// equivalent. +AlternativeBillingOnlyReportingDetailsWrapper + alternativeBillingOnlyReportingDetailsWrapperFromPlatform( + PlatformAlternativeBillingOnlyReportingDetailsResponse response) { + return AlternativeBillingOnlyReportingDetailsWrapper( + responseCode: const BillingResponseConverter() + .fromJson(response.billingResult.responseCode), + debugMessage: response.billingResult.debugMessage, + externalTransactionToken: response.externalTransactionToken, + ); +} + +/// Creates a [BillingConfigWrapper] from the Pigeon equivalent. +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( + productId: product.productId, + productType: platformProductTypeFromWrapper(product.productType), + ); +} + +/// Converts a [ProductType] to its Pigeon equivalent. +PlatformProductType platformProductTypeFromWrapper(ProductType type) { + return switch (type) { + ProductType.inapp => PlatformProductType.inapp, + ProductType.subs => PlatformProductType.subs, + }; +} + +/// Creates a [UserChoiceDetailsWrapper] from the Pigeon equivalent. +UserChoiceDetailsWrapper userChoiceDetailsFromPlatform( + PlatformUserChoiceDetails details) { + return UserChoiceDetailsWrapper( + originalExternalTransactionId: details.originalExternalTransactionId ?? '', + externalTransactionToken: details.externalTransactionToken, + // See TODOs in messages.dart for why this is currently JSON. + products: details.productsJsonList + .map((Object? json) => UserChoiceDetailsProductWrapper.fromJson( + (json! as Map).cast())) + .toList(), + ); +} 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 0000000000..1236b63caf --- /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 0000000000..7d66a8723d --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -0,0 +1,266 @@ +// 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', +)) + +/// 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( + {required this.responseCode, required this.debugMessage}); + final int responseCode; + 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 it as non-nullable. + 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 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({ + 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 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 { + 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 it as non-nullable. + final List purchasesJsonList; +} + +/// Pigeon version of UserChoiceDetailsWrapper and Java UserChoiceDetails. +class PlatformUserChoiceDetails { + PlatformUserChoiceDetails({ + required this.originalExternalTransactionId, + required this.externalTransactionToken, + required this.productsJsonList, + }); + + final String? originalExternalTransactionId; + final String externalTransactionToken; + + /// A JSON-compatible list of products, where each entry in the list is a + /// Map JSON encoding of the product. + // 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 productsJsonList; +} + +/// 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. + /// + /// Default state. + playBillingOnly, + + /// Billing through app provided flow. + alternativeBillingOnly, + + /// Users can choose Play billing or alternative billing. + userChoiceBilling, +} + +@HostApi() +abstract class InAppPurchaseApi { + /// Wraps BillingClient#isReady. + bool isReady(); + + /// Wraps BillingClient#startConnection(BillingClientStateListener). + @async + PlatformBillingResult startConnection( + int callbackHandle, PlatformBillingChoiceMode billingMode); + + /// Wraps BillingClient#endConnection(BillingClientStateListener). + void endConnection(); + + /// Wraps BillingClient#getBillingConfigAsync(GetBillingConfigParams, BillingConfigResponseListener). + @async + PlatformBillingConfigResponse getBillingConfigAsync(); + + /// Wraps BillingClient#launchBillingFlow(Activity, BillingFlowParams). + PlatformBillingResult launchBillingFlow(PlatformBillingFlowParams params); + + /// Wraps BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener). + @async + PlatformBillingResult acknowledgePurchase(String purchaseToken); + + /// Wraps BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener). + @async + PlatformBillingResult consumeAsync(String purchaseToken); + + /// Wraps BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener). + @async + PlatformPurchasesResponse queryPurchasesAsync( + PlatformProductType productType); + + /// Wraps BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener). + @async + PlatformPurchaseHistoryResponse queryPurchaseHistoryAsync( + PlatformProductType productType); + + /// 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 + // from the Play Billing API aren't duplicated in Dart code. + bool isFeatureSupported(String feature); + + /// Wraps BillingClient#isAlternativeBillingOnlyAvailableAsync(). + @async + PlatformBillingResult isAlternativeBillingOnlyAvailableAsync(); + + /// Wraps BillingClient#showAlternativeBillingOnlyInformationDialog(). + @async + PlatformBillingResult showAlternativeBillingOnlyInformationDialog(); + + /// Wraps BillingClient#createAlternativeBillingOnlyReportingDetailsAsync(AlternativeBillingOnlyReportingDetailsListener). + @async + PlatformAlternativeBillingOnlyReportingDetailsResponse + createAlternativeBillingOnlyReportingDetailsAsync(); +} + +@FlutterApi() +abstract class InAppPurchaseCallbackApi { + /// Called for BillingClientStateListener#onBillingServiceDisconnected(). + void onBillingServiceDisconnected(int callbackHandle); + + /// Called for PurchasesUpdatedListener#onPurchasesUpdated(BillingResult, List). + void onPurchasesUpdated(PlatformPurchasesResponse update); + + /// Called for UserChoiceBillingListener#userSelectedAlternativeBilling(UserChoiceDetails). + void userSelectedalternativeBilling(PlatformUserChoiceDetails details); +} 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 5f57552f23..54da00c680 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 @@ -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_manager_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart index 81874d75d8..1c72d02f99 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,54 +4,44 @@ import 'dart:async'; -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase_android/billing_client_wrappers.dart'; -import 'package:in_app_purchase_android/src/channel.dart'; +import '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()'; - - setUpAll(() => TestDefaultBinaryMessengerBinding - .instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, stubPlatform.fakeMethodCallHandler)); setUp(() { WidgetsFlutterBinding.ensureInitialized(); - connectedCompleter = Completer.sync(); - stubPlatform.addResponse( - name: startConnectionCall, - value: buildBillingResultMap( - const BillingResultWrapper(responseCode: BillingResponse.ok), - ), - additionalStepBeforeReturn: (dynamic _) => connectedCompleter.future, - ); - stubPlatform.addResponse(name: endConnectionCall); - manager = BillingClientManager(); + mockApi = MockInAppPurchaseApi(); + when(mockApi.startConnection(any, any)).thenAnswer( + (_) async => PlatformBillingResult(responseCode: 0, debugMessage: '')); + manager = BillingClientManager( + billingClientFactory: (PurchasesUpdatedListener listener, + UserSelectedAlternativeBillingListener? + alternativeBillingListener) => + BillingClient(listener, alternativeBillingListener, 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 PlatformBillingResult(responseCode: 0, debugMessage: ''); + }); + final Completer calledCompleter1 = Completer(); final Completer calledCompleter2 = Completer(); unawaited(manager.runWithClient((BillingClient _) async { @@ -70,51 +60,39 @@ void main() { test('re-connects when client sends onBillingServiceDisconnected', () async { - connectedCompleter.complete(); // Ensures all asynchronous connected code finishes. await manager.runWithClientNonRetryable((_) async {}); - await manager.client.callHandler( - const MethodCall(onBillingServiceDisconnectedCallback, - {'handle': 0}), - ); - expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(2)); + manager.client.hostCallbackHandler.onBillingServiceDisconnected(0); + 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 {}); await manager.reconnectWithBillingChoiceMode( BillingChoiceMode.alternativeBillingOnly); // Verify that connection was ended. - expect(stubPlatform.countPreviousCalls(endConnectionCall), equals(1)); + verify(mockApi.endConnection()).called(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( - const MethodCall(onBillingServiceDisconnectedCallback, - {'handle': 0}), - ); + manager.client.hostCallbackHandler.onBillingServiceDisconnected(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,23 +104,22 @@ 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)); - expect(stubPlatform.countPreviousCalls(endConnectionCall), equals(1)); + verifyNever(mockApi.startConnection(any, any)); + verify(mockApi.endConnection()).called(1); }); test( 'Emits UserChoiceDetailsWrapper when onUserChoiceAlternativeBilling is called', () async { - connectedCompleter.complete(); // Ensures all asynchronous connected code finishes. await manager.runWithClientNonRetryable((_) 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 92f14e2958..4bf15d878b 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 @@ -4,13 +4,15 @@ 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'; -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'; import 'purchase_wrapper_test.dart'; @@ -28,30 +30,30 @@ const PurchaseWrapper dummyOldPurchase = PurchaseWrapper( purchaseState: PurchaseStateWrapper.purchased, ); +@GenerateNiceMocks(>[MockSpec()]) 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 _) {}, (UserChoiceDetailsWrapper _) {}); - stubPlatform.reset(); + (PurchasesResultWrapper _) {}, (UserChoiceDetailsWrapper _) {}, + api: mockApi); }); 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); }); }); @@ -76,17 +78,14 @@ 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: { - 'responseCode': const BillingResponseConverter().toJson(responseCode), - 'debugMessage': debugMessage, - }, + when(mockApi.startConnection(any, any)).thenAnswer( + (_) async => PlatformBillingResult( + responseCode: const BillingResponseConverter().toJson(responseCode), + debugMessage: debugMessage, + ), ); const BillingResultWrapper billingResult = BillingResultWrapper( @@ -97,74 +96,38 @@ 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 alternativeBillingOnly 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, - })); + + expect(verify(mockApi.startConnection(any, captureAny)).captured.first, + PlatformBillingChoiceMode.alternativeBillingOnly); }); test('passes billingChoiceMode userChoiceBilling when set', () async { - const String debugMessage = 'dummy message'; - const BillingResponse responseCode = BillingResponse.ok; - stubPlatform.addResponse( - name: methodName, - value: { - 'responseCode': const BillingResponseConverter().toJson(responseCode), - 'debugMessage': debugMessage, - }, - ); final Completer completer = Completer(); - billingClient = BillingClient((PurchasesResultWrapper _) {}, - (UserChoiceDetailsWrapper details) => completer.complete(details)); - stubPlatform.reset(); + (UserChoiceDetailsWrapper details) => completer.complete(details), + api: mockApi); + await billingClient.startConnection( onBillingServiceDisconnected: () {}, - billingChoiceMode: BillingChoiceMode.userChoiceBilling); - final MethodCall call = stubPlatform.previousCallMatching(methodName); - expect( - call.arguments, - equals({ - 'handle': 0, - 'billingChoiceMode': 2, - })); + billingChoiceMode: BillingChoiceMode.alternativeBillingOnly); + + expect(verify(mockApi.startConnection(any, captureAny)).captured.first, + PlatformBillingChoiceMode.alternativeBillingOnly); + const UserChoiceDetailsWrapper expected = UserChoiceDetailsWrapper( originalExternalTransactionId: 'TransactionId', externalTransactionToken: 'TransactionToken', @@ -179,8 +142,7 @@ void main() { productType: ProductType.inapp), ], ); - await billingClient.callHandler( - MethodCall(kUserSelectedAlternativeBilling, expected.toJson())); + billingClient.hostCallbackHandler.alternativeBillingListener!(expected); expect(completer.isCompleted, isTrue); expect(await completer.future, expected); }); @@ -223,43 +185,26 @@ void main() { expect(productJson, contains(productOfferTokenKey)); expect(productJson, contains(productTypeKey)); }); - - test('handles method channel returning null', () async { - stubPlatform.addResponse( - name: methodName, - ); - - expect( - await billingClient.startConnection( - onBillingServiceDisconnected: () {}), - equals(const BillingResultWrapper( - responseCode: BillingResponse.error, - debugMessage: kInvalidBillingResultErrorMessage))); - }); }); 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', () { - 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: [ @@ -276,15 +221,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( @@ -299,39 +245,16 @@ 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', () { - 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'; @@ -342,25 +265,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'; @@ -389,10 +306,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'; @@ -405,15 +320,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( @@ -423,10 +338,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'; @@ -442,16 +355,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)); }); @@ -462,10 +375,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'; @@ -481,16 +392,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)); }); @@ -499,41 +410,24 @@ 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); }); }); group('queryPurchases', () { - const String queryPurchasesMethodName = - 'BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)'; - test('serializes and deserializes data', () async { const BillingResponse expectedCode = BillingResponse.ok; final List expectedList = [ @@ -542,14 +436,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); @@ -564,42 +460,27 @@ 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); }); }); group('queryPurchaseHistory', () { - const String queryPurchaseHistoryMethodName = - 'BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener)'; - test('serializes and deserializes data', () async { const BillingResponse expectedCode = BillingResponse.ok; final List expectedList = @@ -609,15 +490,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); @@ -630,12 +513,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); @@ -643,118 +528,57 @@ 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', () { - 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', () { - 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', () { - 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')); }); }); @@ -764,51 +588,26 @@ 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', () { 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.isAlternativeBillingOnlyAvailableAsync()).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', () { @@ -818,67 +617,49 @@ void main() { responseCode: BillingResponse.ok, debugMessage: 'debug', externalTransactionToken: 'abc123youandme'); - stubPlatform.addResponse( - name: BillingClient - .createAlternativeBillingOnlyReportingDetailsMethodString, - value: buildAlternativeBillingOnlyReportingDetailsMap(expected)); + when(mockApi.createAlternativeBillingOnlyReportingDetailsAsync()) + .thenAnswer((_) async => + platformAlternativeBillingOnlyReportingDetailsFromWrapper( + 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', () { 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); - }); }); } -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); } -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 + platformAlternativeBillingOnlyReportingDetailsFromWrapper( + 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 new file mode 100644 index 0000000000..133c65445b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart @@ -0,0 +1,428 @@ +// 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 + +class _FakePlatformBillingResult_0 extends _i1.SmartFake + implements _i2.PlatformBillingResult { + _FakePlatformBillingResult_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformBillingConfigResponse_1 extends _i1.SmartFake + implements _i2.PlatformBillingConfigResponse { + _FakePlatformBillingConfigResponse_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformPurchasesResponse_2 extends _i1.SmartFake + implements _i2.PlatformPurchasesResponse { + _FakePlatformPurchasesResponse_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +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_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_5 + extends _i1.SmartFake + implements _i2.PlatformAlternativeBillingOnlyReportingDetailsResponse { + _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [InAppPurchaseApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { + @override + _i3.Future isReady() => (super.noSuchMethod( + Invocation.method( + #isReady, + [], + ), + returnValue: _i3.Future.value(false), + returnValueForMissingStub: _i3.Future.value(false), + ) as _i3.Future); + + @override + _i3.Future<_i2.PlatformBillingResult> startConnection( + int? callbackHandle, + _i2.PlatformBillingChoiceMode? billingMode, + ) => + (super.noSuchMethod( + Invocation.method( + #startConnection, + [ + callbackHandle, + billingMode, + ], + ), + 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.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) => + (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) => + (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( + 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.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.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) => + (super.noSuchMethod( + Invocation.method( + #queryProductDetailsAsync, + [products], + ), + returnValue: _i3.Future<_i2.PlatformProductDetailsResponse>.value( + _FakePlatformProductDetailsResponse_4( + this, + Invocation.method( + #queryProductDetailsAsync, + [products], + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.PlatformProductDetailsResponse>.value( + _FakePlatformProductDetailsResponse_4( + this, + Invocation.method( + #queryProductDetailsAsync, + [products], + ), + )), + ) as _i3.Future<_i2.PlatformProductDetailsResponse>); + + @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> + 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> + 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>); + + @override + _i3.Future<_i2.PlatformAlternativeBillingOnlyReportingDetailsResponse> + createAlternativeBillingOnlyReportingDetailsAsync() => + (super.noSuchMethod( + Invocation.method( + #createAlternativeBillingOnlyReportingDetailsAsync, + [], + ), + returnValue: _i3.Future< + _i2 + .PlatformAlternativeBillingOnlyReportingDetailsResponse>.value( + _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_5( + this, + Invocation.method( + #createAlternativeBillingOnlyReportingDetailsAsync, + [], + ), + )), + returnValueForMissingStub: _i3.Future< + _i2 + .PlatformAlternativeBillingOnlyReportingDetailsResponse>.value( + _FakePlatformAlternativeBillingOnlyReportingDetailsResponse_5( + this, + Invocation.method( + #createAlternativeBillingOnlyReportingDetailsAsync, + [], + ), + )), + ) as _i3.Future< + _i2.PlatformAlternativeBillingOnlyReportingDetailsResponse>); +} 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 bf6612542b..6040318103 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,57 +8,43 @@ 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:in_app_purchase_android/src/types/translator.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 startConnectionCall = - 'BillingClient#startConnection(BillingClientStateListener)'; - const String endConnectionCall = 'BillingClient#endConnection()'; - const String onBillingServiceDisconnectedCallback = - 'BillingClientStateListener#onBillingServiceDisconnected()'; late BillingClientManager manager; - setUpAll(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, stubPlatform.fakeMethodCallHandler); - }); - 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(); + when(mockApi.startConnection(any, any)).thenAnswer( + (_) async => PlatformBillingResult(responseCode: 0, debugMessage: '')); + manager = BillingClientManager( + billingClientFactory: (PurchasesUpdatedListener listener, + UserSelectedAlternativeBillingListener? + alternativeBillingListener) => + BillingClient(listener, alternativeBillingListener, api: mockApi)); iapAndroidPlatformAddition = InAppPurchaseAndroidPlatformAddition(manager); }); 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); @@ -75,10 +61,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(); @@ -87,53 +71,33 @@ void main() { }); group('setBillingChoice', () { - late Map arguments; test('setAlternativeBillingOnlyState', () async { - stubPlatform.reset(); - stubPlatform.addResponse( - name: startConnectionCall, - additionalStepBeforeReturn: (dynamic value) => - arguments = value as Map, - ); - stubPlatform.addResponse(name: endConnectionCall); + clearInteractions(mockApi); await iapAndroidPlatformAddition .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. - 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, - ); - stubPlatform.addResponse(name: endConnectionCall); + clearInteractions(mockApi); await iapAndroidPlatformAddition .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. - 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); }); }); @@ -141,11 +105,10 @@ void main() { test('isAlternativeBillingOnlyAvailable success', () async { const BillingResultWrapper expected = BillingResultWrapper( responseCode: BillingResponse.ok, debugMessage: 'dummy message'); + when(mockApi.isAlternativeBillingOnlyAvailableAsync()).thenAnswer( + (_) async => PlatformBillingResult( + responseCode: 0, debugMessage: expected.debugMessage!)); - stubPlatform.addResponse( - name: BillingClient.isAlternativeBillingOnlyAvailableMethodString, - value: buildBillingResultMap(expected), - ); final BillingResultWrapper result = await iapAndroidPlatformAddition.isAlternativeBillingOnlyAvailable(); @@ -158,11 +121,10 @@ void main() { const BillingResultWrapper expected = BillingResultWrapper( responseCode: BillingResponse.ok, debugMessage: 'dummy message'); - stubPlatform.addResponse( - name: BillingClient - .showAlternativeBillingOnlyInformationDialogMethodString, - value: buildBillingResultMap(expected), - ); + when(mockApi.isAlternativeBillingOnlyAvailableAsync()) + .thenAnswer((_) async => convertToPigeonResult(expected)); + when(mockApi.showAlternativeBillingOnlyInformationDialog()) + .thenAnswer((_) async => convertToPigeonResult(expected)); final BillingResultWrapper result = await iapAndroidPlatformAddition.isAlternativeBillingOnlyAvailable(); @@ -172,43 +134,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. @@ -219,26 +158,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); @@ -252,34 +178,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 b45efcf344..7102133133 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,124 +9,103 @@ 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'; +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 startConnectionCall = - 'BillingClient#startConnection(BillingClientStateListener)'; - const String endConnectionCall = 'BillingClient#endConnection()'; - const String acknowledgePurchaseCall = - 'BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; - const String onBillingServiceDisconnectedCallback = - 'BillingClientStateListener#onBillingServiceDisconnected()'; - - setUpAll(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, stubPlatform.fakeMethodCallHandler); - }); 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); - - InAppPurchaseAndroidPlatform.registerPlatform(); - iapAndroidPlatform = - InAppPurchasePlatform.instance as InAppPurchaseAndroidPlatform; - }); - - tearDown(() { - stubPlatform.reset(); + mockApi = MockInAppPurchaseApi(); + when(mockApi.startConnection(any, any)).thenAnswer( + (_) async => PlatformBillingResult(responseCode: 0, debugMessage: '')); + iapAndroidPlatform = InAppPurchaseAndroidPlatform( + manager: BillingClientManager( + billingClientFactory: (PurchasesUpdatedListener listener, + UserSelectedAlternativeBillingListener? + alternativeBillingListener) => + BillingClient(listener, alternativeBillingListener, + api: mockApi))); + InAppPurchasePlatform.instance = iapAndroidPlatform; }); 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', () { - iapAndroidPlatform.billingClientManager.client.callHandler( - const MethodCall(onBillingServiceDisconnectedCallback, - {'handle': 0}), - ); - expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(2)); + iapAndroidPlatform.billingClientManager.client.hostCallbackHandler + .onBillingServiceDisconnected(0); + verify(mockApi.startConnection(any, any)).called(2); }); test( 're-connects when operation returns BillingResponse.clientDisconnected', () async { - final Map okValue = buildBillingResultMap( - const BillingResultWrapper(responseCode: BillingResponse.ok)); - stubPlatform.addResponse( - name: acknowledgePurchaseCall, - value: buildBillingResultMap( - const BillingResultWrapper( - responseCode: BillingResponse.serviceDisconnected, - ), - ), - ); - stubPlatform.addResponse( - name: startConnectionCall, - value: okValue, - additionalStepBeforeReturn: (dynamic _) => stubPlatform.addResponse( - name: acknowledgePurchaseCall, value: okValue), + when(mockApi.acknowledgePurchase(any)).thenAnswer( + (_) async => PlatformBillingResult( + responseCode: const BillingResponseConverter() + .toJson(BillingResponse.serviceDisconnected), + debugMessage: 'disconnected'), ); + when(mockApi.startConnection(any, any)).thenAnswer((_) async { + // 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 = GooglePlayPurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase) .first; final BillingResultWrapper result = await iapAndroidPlatform.completePurchase(purchase); - expect( - stubPlatform.countPreviousCalls(acknowledgePurchaseCall), - equals(2), - ); - expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(2)); + verify(mockApi.acknowledgePurchase(any)).called(2); + verify(mockApi.startConnection(any, any)).called(2); expect(result.responseCode, equals(BillingResponse.ok)); }); }); 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); }); }); 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({''}); @@ -136,14 +115,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 = @@ -162,14 +143,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 = @@ -180,23 +163,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 = @@ -212,55 +185,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(), @@ -291,16 +223,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 @@ -328,11 +261,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; const String accountId = 'hashedAccountId'; @@ -341,33 +269,30 @@ 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. + iapAndroidPlatform.billingClientManager.client.hostCallbackHandler + .onPurchasesUpdated(PlatformPurchasesResponse( + billingResult: convertToPigeonResult(expectedBillingResult), + purchasesJsonList: [ + { + 'orderId': 'orderID1', + 'products': [productDetails.productId], + 'isAutoRenewing': false, + 'packageName': 'package', + 'purchaseTime': 1231231231, + 'purchaseToken': 'token', + 'signature': 'sign', + 'originalJson': 'json', + 'developerPayload': 'dummy payload', + 'isAcknowledged': true, + 'purchaseState': 1, + } + ], + )); + + return convertToPigeonResult(expectedBillingResult); + }); final Completer completer = Completer(); PurchaseDetails purchaseDetails; final Stream> purchaseStream = @@ -400,19 +325,16 @@ 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. + iapAndroidPlatform.billingClientManager.client.hostCallbackHandler + .onPurchasesUpdated(PlatformPurchasesResponse( + billingResult: convertToPigeonResult(expectedBillingResult), + purchasesJsonList: [], + )); + + return convertToPigeonResult(expectedBillingResult); + }); final Completer completer = Completer(); PurchaseDetails purchaseDetails; final Stream> purchaseStream = @@ -445,47 +367,42 @@ 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. + iapAndroidPlatform.billingClientManager.client.hostCallbackHandler + .onPurchasesUpdated(PlatformPurchasesResponse( + billingResult: convertToPigeonResult(expectedBillingResult), + purchasesJsonList: [ + { + 'orderId': 'orderID1', + 'products': [productDetails.productId], + 'isAutoRenewing': false, + 'packageName': 'package', + 'purchaseTime': 1231231231, + 'purchaseToken': 'token', + 'signature': 'sign', + 'originalJson': 'json', + 'developerPayload': 'dummy payload', + 'isAcknowledged': true, + 'purchaseState': 1, + } + ], + )); + + return convertToPigeonResult(expectedBillingResult); + }); final Completer consumeCompleter = Completer(); // adding call back for consume purchase const BillingResponse expectedCode = BillingResponse.ok; 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; @@ -521,9 +438,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( @@ -541,10 +457,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( @@ -563,47 +477,42 @@ 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. + iapAndroidPlatform.billingClientManager.client.hostCallbackHandler + .onPurchasesUpdated(PlatformPurchasesResponse( + billingResult: convertToPigeonResult(expectedBillingResult), + purchasesJsonList: [ + { + 'orderId': 'orderID1', + 'products': [productDetails.productId], + 'isAutoRenewing': false, + 'packageName': 'package', + 'purchaseTime': 1231231231, + 'purchaseToken': 'token', + 'signature': 'sign', + 'originalJson': 'json', + 'developerPayload': 'dummy payload', + 'isAcknowledged': true, + 'purchaseState': 1, + } + ], + )); + + return convertToPigeonResult(expectedBillingResult); + }); final Completer consumeCompleter = Completer(); // adding call back for consume purchase const BillingResponse expectedCode = BillingResponse.error; 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; @@ -642,47 +551,42 @@ 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. + iapAndroidPlatform.billingClientManager.client.hostCallbackHandler + .onPurchasesUpdated(PlatformPurchasesResponse( + billingResult: convertToPigeonResult(expectedBillingResult), + purchasesJsonList: [ + { + 'orderId': 'orderID1', + 'products': [productDetails.productId], + 'isAutoRenewing': false, + 'packageName': 'package', + 'purchaseTime': 1231231231, + 'purchaseToken': 'token', + 'signature': 'sign', + 'originalJson': 'json', + 'developerPayload': 'dummy payload', + 'isAcknowledged': true, + 'purchaseState': 1, + } + ], + )); + + return convertToPigeonResult(expectedBillingResult); + }); final Completer consumeCompleter = Completer(); // adding call back for consume purchase const BillingResponse expectedCode = BillingResponse.ok; 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; @@ -709,47 +613,42 @@ 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. + iapAndroidPlatform.billingClientManager.client.hostCallbackHandler + .onPurchasesUpdated(PlatformPurchasesResponse( + billingResult: convertToPigeonResult(expectedBillingResult), + purchasesJsonList: [ + { + 'orderId': 'orderID1', + 'products': [productDetails.productId], + 'isAutoRenewing': false, + 'packageName': 'package', + 'purchaseTime': 1231231231, + 'purchaseToken': 'token', + 'signature': 'sign', + 'originalJson': 'json', + 'developerPayload': 'dummy payload', + 'isAcknowledged': true, + 'purchaseState': 1, + } + ], + )); + + return convertToPigeonResult(expectedBillingResult); + }); final Completer consumeCompleter = Completer(); // adding call back for consume purchase const BillingResponse expectedCode = BillingResponse.userCanceled; 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; @@ -782,19 +681,16 @@ 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. + iapAndroidPlatform.billingClientManager.client.hostCallbackHandler + .onPurchasesUpdated(PlatformPurchasesResponse( + billingResult: convertToPigeonResult(expectedBillingResult), + purchasesJsonList: [], + )); + + return convertToPigeonResult(expectedBillingResult); + }); final Completer completer = Completer(); PurchaseDetails purchaseDetails; @@ -824,17 +720,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/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 35e2807bc3..0000000000 --- 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); - } - } -} 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 0000000000..827ce25d74 --- /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!, + ); +}