[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
This commit is contained in:
stuartmorgan
2024-04-03 10:49:09 -04:00
committed by GitHub
parent f4790cfd02
commit 0e848facdb
28 changed files with 5092 additions and 2242 deletions

View File

@ -1,3 +1,7 @@
## 0.3.2+1
* Converts internal platform communication to Pigeon.
## 0.3.2
* Adds UserChoiceBilling APIs to platform addition.

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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<String, ProductDetails> 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<Product> 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<PlatformBillingResult> 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;
}
try {
billingClient.showAlternativeBillingOnlyInformationDialog(
activity,
billingResult -> {
result.success(fromBillingResult(billingResult));
});
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<Messages.PlatformAlternativeBillingOnlyReportingDetailsResponse> result) {
if (billingClient == null) {
result.error(getNullBillingClientError());
return;
}
try {
billingClient.createAlternativeBillingOnlyReportingDetailsAsync(
((billingResult, alternativeBillingOnlyReportingDetails) -> {
((billingResult, alternativeBillingOnlyReportingDetails) ->
result.success(
fromAlternativeBillingOnlyReportingDetails(
billingResult, alternativeBillingOnlyReportingDetails));
}));
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<PlatformBillingResult> result) {
if (billingClient == null) {
result.error(getNullBillingClientError());
return;
}
try {
billingClient.isAlternativeBillingOnlyAvailableAsync(
billingResult -> {
result.success(fromBillingResult(billingResult));
});
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<Messages.PlatformBillingConfigResponse> result) {
if (billingClient == null) {
result.error(getNullBillingClientError());
return;
}
try {
billingClient.getBillingConfigAsync(
GetBillingConfigParams.newBuilder().build(),
(billingResult, billingConfig) -> {
result.success(fromBillingConfig(billingResult, billingConfig));
});
(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;
}
result.success(billingClient.isReady());
}
private void queryProductDetailsAsync(
final List<Product> productList, final MethodChannel.Result result) {
if (billingClientError(result)) {
@Override
@NonNull
public Boolean isReady() {
if (billingClient == null) {
throw getNullBillingClientError();
}
return billingClient.isReady();
}
@Override
public void queryProductDetailsAsync(
@NonNull List<PlatformProduct> products,
@NonNull Result<PlatformProductDetailsResponse> result) {
if (billingClient == null) {
result.error(getNullBillingClientError());
return;
}
try {
QueryProductDetailsParams params =
QueryProductDetailsParams.newBuilder().setProductList(productList).build();
QueryProductDetailsParams.newBuilder().setProductList(toProductList(products)).build();
billingClient.queryProductDetailsAsync(
params,
(billingResult, productDetailsList) -> {
updateCachedProducts(productDetailsList);
final Map<String, Object> productDetailsResponse = new HashMap<>();
productDetailsResponse.put("billingResult", fromBillingResult(billingResult));
productDetailsResponse.put(
"productDetailsList", fromProductDetailsList(productDetailsList));
result.success(productDetailsResponse);
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<BillingFlowParams.ProductDetailsParams> 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,76 +356,94 @@ class MethodCallHandlerImpl
builder.setReplaceProrationMode(prorationMode);
}
private void consumeAsync(String purchaseToken, final MethodChannel.Result result) {
if (billingClientError(result)) {
@Override
public void consumeAsync(
@NonNull String purchaseToken, @NonNull Result<PlatformBillingResult> result) {
if (billingClient == null) {
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)));
}
}
private void queryPurchasesAsync(String productType, MethodChannel.Result result) {
if (billingClientError(result)) {
@Override
public void queryPurchasesAsync(
@NonNull PlatformProductType productType,
@NonNull Result<Messages.PlatformPurchasesResponse> 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(productType);
paramsBuilder.setProductType(toProductTypeString(productType));
billingClient.queryPurchasesAsync(
paramsBuilder.build(),
(billingResult, purchasesList) -> {
final Map<String, Object> serialized = new HashMap<>();
// The response code is no longer passed, as part of billing 4.0, so we pass OK here
// as success is implied by calling this callback.
serialized.put("responseCode", BillingClient.BillingResponseCode.OK);
serialized.put("billingResult", fromBillingResult(billingResult));
serialized.put("purchasesList", fromPurchasesList(purchasesList));
result.success(serialized);
PlatformPurchasesResponse.Builder builder =
new PlatformPurchasesResponse.Builder()
.setBillingResult(fromBillingResult(billingResult))
.setPurchasesJsonList(fromPurchasesList(purchasesList));
result.success(builder.build());
});
} catch (RuntimeException e) {
result.error(new FlutterError("error", e.getMessage(), Log.getStackTraceString(e)));
}
}
private void queryPurchaseHistoryAsync(String productType, final MethodChannel.Result result) {
if (billingClientError(result)) {
return;
}
billingClient.queryPurchaseHistoryAsync(
QueryPurchaseHistoryParams.newBuilder().setProductType(productType).build(),
(billingResult, purchasesList) -> {
final Map<String, Object> 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<String, Object> 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 queryPurchaseHistoryAsync(
@NonNull PlatformProductType productType,
@NonNull Result<Messages.PlatformPurchaseHistoryResponse> result) {
if (billingClient == null) {
UserChoiceBillingListener listener = getUserChoiceBillingListener(billingChoiceMode);
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<PlatformBillingResult> result) {
if (billingClient == null) {
UserChoiceBillingListener listener = getUserChoiceBillingListener(billingMode);
billingClient =
billingClientFactory.createBillingClient(
applicationContext, methodChannel, billingChoiceMode, listener);
applicationContext, callbackApi, billingMode, listener);
}
try {
billingClient.startConnection(
new BillingClientStateListener() {
private boolean alreadyFinished = false;
@ -536,34 +462,65 @@ class MethodCallHandlerImpl
@Override
public void onBillingServiceDisconnected() {
final Map<String, Object> arguments = new HashMap<>();
arguments.put("handle", handle);
methodChannel.invokeMethod(MethodNames.ON_DISCONNECT, arguments);
callbackApi.onBillingServiceDisconnected(
handle,
new Messages.VoidResult() {
@Override
public void success() {}
@Override
public void error(@NonNull Throwable error) {
io.flutter.Log.e(
"IN_APP_PURCHASE",
"onBillingServiceDisconnected handler error: " + error);
}
});
}
});
} 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<String, Object> 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<PlatformBillingResult> result) {
if (billingClient == null) {
result.error(getNullBillingClientError());
return;
}
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<ProductDetails> productDetailsList) {
@ -576,21 +533,16 @@ class MethodCallHandlerImpl
}
}
private boolean billingClientError(MethodChannel.Result result) {
if (billingClient != null) {
return false;
private @NonNull FlutterError getNullBillingClientError() {
return new FlutterError("UNAVAILABLE", "BillingClient is unset. Try reconnecting.", null);
}
result.error("UNAVAILABLE", "BillingClient is unset. Try reconnecting.", null);
return true;
@Override
public @NonNull Boolean isFeatureSupported(@NonNull String feature) {
if (billingClient == null) {
throw getNullBillingClientError();
}
private void isFeatureSupported(String feature, MethodChannel.Result result) {
if (billingClientError(result)) {
return;
}
assert billingClient != null;
BillingResult billingResult = billingClient.isFeatureSupported(feature);
result.success(billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK);
return billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK;
}
}

View File

@ -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<Purchase>)";
PluginPurchaseListener(MethodChannel channel) {
this.channel = channel;
PluginPurchaseListener(Messages.InAppPurchaseCallbackApi callbackApi) {
this.callbackApi = callbackApi;
}
@Override
public void onPurchasesUpdated(
@NonNull BillingResult billingResult, @Nullable List<Purchase> purchases) {
final Map<String, Object> 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);
}
});
}
}

View File

@ -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<QueryProductDetailsParams.Product> toProductList(List<Object> serialized) {
static List<QueryProductDetailsParams.Product> toProductList(
List<Messages.PlatformProduct> platformProducts) {
List<QueryProductDetailsParams.Product> products = new ArrayList<>();
for (Object productSerialized : serialized) {
@SuppressWarnings(value = "unchecked")
Map<String, Object> productMap = (Map<String, Object>) productSerialized;
products.add(toProduct(productMap));
for (Messages.PlatformProduct platformProduct : platformProducts) {
products.add(toProduct(platformProduct));
}
return products;
}
static QueryProductDetailsParams.Product toProduct(Map<String, Object> 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<HashMap<String, Object>> fromProductDetailsList(
@Nullable List<ProductDetails> 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<Object> fromProductDetailsList(@Nullable List<ProductDetails> productDetailsList) {
if (productDetailsList == null) {
return Collections.emptyList();
}
ArrayList<HashMap<String, Object>> output = new ArrayList<>();
// This and the method are generically typed due to Pigeon limitations; see
// https://github.com/flutter/flutter/issues/116117.
ArrayList<Object> output = new ArrayList<>();
for (ProductDetails detail : productDetailsList) {
output.add(fromProductDetail(detail));
}
@ -203,51 +212,59 @@ import java.util.Map;
return info;
}
static List<HashMap<String, Object>> fromPurchasesList(@Nullable List<Purchase> purchases) {
static List<Object> fromPurchasesList(@Nullable List<Purchase> purchases) {
if (purchases == null) {
return Collections.emptyList();
}
List<HashMap<String, Object>> serialized = new ArrayList<>();
// This and the method are generically typed due to Pigeon limitations; see
// https://github.com/flutter/flutter/issues/116117.
List<Object> serialized = new ArrayList<>();
for (Purchase purchase : purchases) {
serialized.add(fromPurchase(purchase));
}
return serialized;
}
static List<HashMap<String, Object>> fromPurchaseHistoryRecordList(
static List<Object> fromPurchaseHistoryRecordList(
@Nullable List<PurchaseHistoryRecord> purchaseHistoryRecords) {
if (purchaseHistoryRecords == null) {
return Collections.emptyList();
}
List<HashMap<String, Object>> serialized = new ArrayList<>();
// This and the method are generically typed due to Pigeon limitations; see
// https://github.com/flutter/flutter/issues/116117.
List<Object> serialized = new ArrayList<>();
for (PurchaseHistoryRecord purchaseHistoryRecord : purchaseHistoryRecords) {
serialized.add(fromPurchaseHistoryRecord(purchaseHistoryRecord));
}
return serialized;
}
static HashMap<String, Object> fromBillingResult(BillingResult billingResult) {
HashMap<String, Object> 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<String, Object> fromUserChoiceDetails(UserChoiceDetails userChoiceDetails) {
HashMap<String, Object> 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<HashMap<String, Object>> fromProductsList(List<Product> productsList) {
static List<Object> fromProductsList(List<Product> productsList) {
if (productsList.isEmpty()) {
return Collections.emptyList();
}
ArrayList<HashMap<String, Object>> output = new ArrayList<>();
// This and the method are generically typed due to Pigeon limitations; see
// https://github.com/flutter/flutter/issues/116117.
ArrayList<Object> 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<String, Object> fromBillingConfig(
static Messages.PlatformBillingConfigResponse fromBillingConfig(
BillingResult result, BillingConfig billingConfig) {
HashMap<String, Object> 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<String, Object> fromAlternativeBillingOnlyReportingDetails(
static Messages.PlatformAlternativeBillingOnlyReportingDetailsResponse
fromAlternativeBillingOnlyReportingDetails(
BillingResult result, AlternativeBillingOnlyReportingDetails details) {
HashMap<String, Object> info = fromBillingResult(result);
if (details != null) {
info.put("externalTransactionToken", details.getExternalTransactionToken());
}
return info;
return new Messages.PlatformAlternativeBillingOnlyReportingDetailsResponse.Builder()
.setBillingResult(fromBillingResult(result))
.setExternalTransactionToken(details.getExternalTransactionToken())
.build();
}
/**

View File

@ -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<HashMap<String, Object>> serialized = Translator.fromProductDetailsList(expected);
final List<Object> 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<HashMap<String, Object>> serialized =
Translator.fromPurchaseHistoryRecordList(expected);
final List<Object> 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<HashMap<String, Object>> serialized = Translator.fromPurchasesList(expected);
final List<Object> 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<String, Object> 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<String, Object> 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<String, Object> serialized) {
assertEquals(expected.getDescription(), serialized.get("description"));
private void assertSerialized(ProductDetails expected, Object serializedGeneric) {
@SuppressWarnings("unchecked")
final Map<String, Object> serialized = (Map<String, Object>) 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<String, Object> serialized) {
private void assertSerialized(Purchase expected, Object serializedGeneric) {
@SuppressWarnings("unchecked")
final Map<String, Object> serialized = (Map<String, Object>) 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<String, Object> serialized) {
private void assertSerialized(PurchaseHistoryRecord expected, Object serializedGeneric) {
@SuppressWarnings("unchecked")
final Map<String, Object> serialized = (Map<String, Object>) serializedGeneric;
assertEquals(expected.getPurchaseTime(), serialized.get("purchaseTime"));
assertEquals(expected.getPurchaseToken(), serialized.get("purchaseToken"));
assertEquals(expected.getSignature(), serialized.get("signature"));

View File

@ -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<PurchasesResultWrapper> _purchasesUpdatedController =
StreamController<PurchasesResultWrapper>.broadcast();
@ -67,6 +84,7 @@ class BillingClientManager {
StreamController<UserChoiceDetailsWrapper>.broadcast();
BillingChoiceMode _billingChoiceMode;
final BillingClientFactory _billingClientFactory;
bool _isConnecting = false;
bool _isDisposed = false;

View File

@ -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<Purchase>)';
/// 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] = <PurchasesUpdatedListener>[
onPurchasesUpdated
];
_callbacks[kUserSelectedAlternativeBilling] = alternativeBillingListener ==
null
? <UserSelectedAlternativeBillingListener>[]
: <UserSelectedAlternativeBillingListener>[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<String, List<Function>> _callbacks = <String, List<Function>>{};
/// 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<bool> isReady() async {
final bool? ready =
await channel.invokeMethod<bool>('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<Function> disconnectCallbacks =
_callbacks[_kOnBillingServiceDisconnected] ??= <Function>[];
_callbacks[_kOnBillingServiceDisconnected]
?.add(onBillingServiceDisconnected);
return BillingResultWrapper.fromJson((await channel
.invokeMapMethod<String, dynamic>(
'BillingClient#startConnection(BillingClientStateListener)',
<String, dynamic>{
'handle': disconnectCallbacks.length - 1,
'billingChoiceMode':
const BillingChoiceModeConverter().toJson(billingChoiceMode),
})) ??
<String, dynamic>{});
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<void> endConnection() async {
return channel.invokeMethod<void>('BillingClient#endConnection()');
return _hostApi.endConnection();
}
/// Returns a list of [ProductDetailsResponseWrapper]s that have
@ -166,16 +137,11 @@ class BillingClient {
Future<ProductDetailsResponseWrapper> queryProductDetails({
required List<ProductWrapper> productList,
}) async {
final Map<String, dynamic> arguments = <String, dynamic>{
'productList':
productList.map((ProductWrapper product) => product.toJson()).toList()
};
return ProductDetailsResponseWrapper.fromJson(
(await channel.invokeMapMethod<String, dynamic>(
'BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener)',
arguments,
)) ??
<String, dynamic>{});
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<String, dynamic> arguments = <String, dynamic>{
'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<String, dynamic>(
'BillingClient#launchBillingFlow(Activity, BillingFlowParams)',
arguments)) ??
<String, dynamic>{});
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<PurchasesResultWrapper> queryPurchases(ProductType productType) async {
return PurchasesResultWrapper.fromJson(
(await channel.invokeMapMethod<String, dynamic>(
'BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)',
<String, dynamic>{
'productType': const ProductTypeConverter().toJson(productType)
},
)) ??
<String, dynamic>{});
// 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<PurchasesHistoryResult> queryPurchaseHistory(
ProductType productType) async {
return PurchasesHistoryResult.fromJson((await channel.invokeMapMethod<
String, dynamic>(
'BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener)',
<String, dynamic>{
'productType': const ProductTypeConverter().toJson(productType)
})) ??
<String, dynamic>{});
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<BillingResultWrapper> consumeAsync(String purchaseToken) async {
return BillingResultWrapper.fromJson((await channel.invokeMapMethod<String,
dynamic>(
'BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener)',
<String, dynamic>{
'purchaseToken': purchaseToken,
})) ??
<String, dynamic>{});
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<BillingResultWrapper> acknowledgePurchase(String purchaseToken) async {
return BillingResultWrapper.fromJson((await channel.invokeMapMethod<String,
dynamic>(
'BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)',
<String, dynamic>{
'purchaseToken': purchaseToken,
})) ??
<String, dynamic>{});
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<bool> isFeatureSupported(BillingClientFeature feature) async {
final bool? result = await channel.invokeMethod<bool>(
'BillingClient#isFeatureSupported(String)', <String, dynamic>{
'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<BillingConfigWrapper> getBillingConfig() async {
return BillingConfigWrapper.fromJson((await channel
.invokeMapMethod<String, dynamic>(getBillingConfigMethodString)) ??
<String, dynamic>{});
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<BillingResultWrapper> isAlternativeBillingOnlyAvailable() async {
return BillingResultWrapper.fromJson(
(await channel.invokeMapMethod<String, dynamic>(
isAlternativeBillingOnlyAvailableMethodString)) ??
<String, dynamic>{});
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<BillingResultWrapper>
showAlternativeBillingOnlyInformationDialog() async {
return BillingResultWrapper.fromJson(
(await channel.invokeMapMethod<String, dynamic>(
showAlternativeBillingOnlyInformationDialogMethodString)) ??
<String, dynamic>{});
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<AlternativeBillingOnlyReportingDetailsWrapper>
createAlternativeBillingOnlyReportingDetails() async {
return AlternativeBillingOnlyReportingDetailsWrapper.fromJson(
(await channel.invokeMapMethod<String, dynamic>(
createAlternativeBillingOnlyReportingDetailsMethodString)) ??
<String, dynamic>{});
return alternativeBillingOnlyReportingDetailsWrapperFromPlatform(
await _hostApi.createAlternativeBillingOnlyReportingDetailsAsync());
}
}
/// The method call handler for [channel].
/// 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
Future<void> 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<dynamic, dynamic>).cast<String, dynamic>()));
case _kOnBillingServiceDisconnected:
final int handle =
(call.arguments as Map<Object?, Object?>)['handle']! as int;
final List<OnBillingServiceDisconnected> onDisconnected =
_callbacks[_kOnBillingServiceDisconnected]!
.cast<OnBillingServiceDisconnected>();
onDisconnected[handle]();
case kUserSelectedAlternativeBilling:
if (_callbacks[kUserSelectedAlternativeBilling]!.isNotEmpty) {
final UserSelectedAlternativeBillingListener listener =
_callbacks[kUserSelectedAlternativeBilling]!.first
as UserSelectedAlternativeBillingListener;
listener(UserChoiceDetailsWrapper.fromJson(
(call.arguments as Map<dynamic, dynamic>)
.cast<String, dynamic>()));
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<OnBillingServiceDisconnected> disconnectCallbacks =
<OnBillingServiceDisconnected>[];
@override
void onBillingServiceDisconnected(int callbackHandle) {
disconnectCallbacks[callbackHandle]();
}
@override
void onPurchasesUpdated(PlatformPurchasesResponse update) {
purchasesUpdatedCallback(purchasesResultWrapperFromPlatform(update));
}
@override
void userSelectedalternativeBilling(PlatformUserChoiceDetails details) {
alternativeBillingListener!(userChoiceDetailsFromPlatform(details));
}
}

View File

@ -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

View File

@ -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,

View File

@ -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');

View File

@ -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<List<PurchaseDetails>> _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<String> _productIdsToConsume = <String>{};

View File

@ -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<Object?> wrapResponse(
{Object? result, PlatformException? error, bool empty = false}) {
if (empty) {
return <Object?>[];
}
if (error == null) {
return <Object?>[result];
}
return <Object?>[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 <Object?>[
productId,
productType.index,
];
}
static PlatformProduct decode(Object result) {
result as List<Object?>;
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 <Object?>[
responseCode,
debugMessage,
];
}
static PlatformBillingResult decode(Object result) {
result as List<Object?>;
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<String, Object?> JSON encoding of the product details.
List<Object?> productDetailsJsonList;
Object encode() {
return <Object?>[
billingResult.encode(),
productDetailsJsonList,
];
}
static PlatformProductDetailsResponse decode(Object result) {
result as List<Object?>;
return PlatformProductDetailsResponse(
billingResult: PlatformBillingResult.decode(result[0]! as List<Object?>),
productDetailsJsonList: (result[1] as List<Object?>?)!.cast<Object?>(),
);
}
}
/// 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 <Object?>[
billingResult.encode(),
externalTransactionToken,
];
}
static PlatformAlternativeBillingOnlyReportingDetailsResponse decode(
Object result) {
result as List<Object?>;
return PlatformAlternativeBillingOnlyReportingDetailsResponse(
billingResult: PlatformBillingResult.decode(result[0]! as List<Object?>),
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 <Object?>[
billingResult.encode(),
countryCode,
];
}
static PlatformBillingConfigResponse decode(Object result) {
result as List<Object?>;
return PlatformBillingConfigResponse(
billingResult: PlatformBillingResult.decode(result[0]! as List<Object?>),
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 <Object?>[
product,
prorationMode,
offerToken,
accountId,
obfuscatedProfileId,
oldProduct,
purchaseToken,
];
}
static PlatformBillingFlowParams decode(Object result) {
result as List<Object?>;
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<String, Object?> JSON encoding of the record.
List<Object?> purchaseHistoryRecordJsonList;
Object encode() {
return <Object?>[
billingResult.encode(),
purchaseHistoryRecordJsonList,
];
}
static PlatformPurchaseHistoryResponse decode(Object result) {
result as List<Object?>;
return PlatformPurchaseHistoryResponse(
billingResult: PlatformBillingResult.decode(result[0]! as List<Object?>),
purchaseHistoryRecordJsonList:
(result[1] as List<Object?>?)!.cast<Object?>(),
);
}
}
/// 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<String, Object?> JSON encoding of the product details.
List<Object?> purchasesJsonList;
Object encode() {
return <Object?>[
billingResult.encode(),
purchasesJsonList,
];
}
static PlatformPurchasesResponse decode(Object result) {
result as List<Object?>;
return PlatformPurchasesResponse(
billingResult: PlatformBillingResult.decode(result[0]! as List<Object?>),
purchasesJsonList: (result[1] as List<Object?>?)!.cast<Object?>(),
);
}
}
/// 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<String, Object?> JSON encoding of the product.
List<Object?> productsJsonList;
Object encode() {
return <Object?>[
originalExternalTransactionId,
externalTransactionToken,
productsJsonList,
];
}
static PlatformUserChoiceDetails decode(Object result) {
result as List<Object?>;
return PlatformUserChoiceDetails(
originalExternalTransactionId: result[0] as String?,
externalTransactionToken: result[1]! as String,
productsJsonList: (result[2] as List<Object?>?)!.cast<Object?>(),
);
}
}
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<Object?> pigeonChannelCodec =
_InAppPurchaseApiCodec();
/// Wraps BillingClient#isReady.
Future<bool> isReady() async {
const String __pigeon_channelName =
'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.isReady';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList =
await __pigeon_channel.send(null) as List<Object?>?;
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<PlatformBillingResult> startConnection(
int callbackHandle, PlatformBillingChoiceMode billingMode) async {
const String __pigeon_channelName =
'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.startConnection';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList = await __pigeon_channel
.send(<Object?>[callbackHandle, billingMode.index]) as List<Object?>?;
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<void> endConnection() async {
const String __pigeon_channelName =
'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.endConnection';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList =
await __pigeon_channel.send(null) as List<Object?>?;
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<PlatformBillingConfigResponse> getBillingConfigAsync() async {
const String __pigeon_channelName =
'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.getBillingConfigAsync';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList =
await __pigeon_channel.send(null) as List<Object?>?;
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<PlatformBillingResult> launchBillingFlow(
PlatformBillingFlowParams params) async {
const String __pigeon_channelName =
'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.launchBillingFlow';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList =
await __pigeon_channel.send(<Object?>[params]) as List<Object?>?;
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<PlatformBillingResult> acknowledgePurchase(
String purchaseToken) async {
const String __pigeon_channelName =
'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.acknowledgePurchase';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList =
await __pigeon_channel.send(<Object?>[purchaseToken]) as List<Object?>?;
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<PlatformBillingResult> consumeAsync(String purchaseToken) async {
const String __pigeon_channelName =
'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.consumeAsync';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList =
await __pigeon_channel.send(<Object?>[purchaseToken]) as List<Object?>?;
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<PlatformPurchasesResponse> queryPurchasesAsync(
PlatformProductType productType) async {
const String __pigeon_channelName =
'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.queryPurchasesAsync';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList = await __pigeon_channel
.send(<Object?>[productType.index]) as List<Object?>?;
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<PlatformPurchaseHistoryResponse> queryPurchaseHistoryAsync(
PlatformProductType productType) async {
const String __pigeon_channelName =
'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.queryPurchaseHistoryAsync';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList = await __pigeon_channel
.send(<Object?>[productType.index]) as List<Object?>?;
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<PlatformProductDetailsResponse> queryProductDetailsAsync(
List<PlatformProduct?> products) async {
const String __pigeon_channelName =
'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.queryProductDetailsAsync';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList =
await __pigeon_channel.send(<Object?>[products]) as List<Object?>?;
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<bool> isFeatureSupported(String feature) async {
const String __pigeon_channelName =
'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.isFeatureSupported';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList =
await __pigeon_channel.send(<Object?>[feature]) as List<Object?>?;
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<PlatformBillingResult> isAlternativeBillingOnlyAvailableAsync() async {
const String __pigeon_channelName =
'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.isAlternativeBillingOnlyAvailableAsync';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList =
await __pigeon_channel.send(null) as List<Object?>?;
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<PlatformBillingResult>
showAlternativeBillingOnlyInformationDialog() async {
const String __pigeon_channelName =
'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.showAlternativeBillingOnlyInformationDialog';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList =
await __pigeon_channel.send(null) as List<Object?>?;
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<PlatformAlternativeBillingOnlyReportingDetailsResponse>
createAlternativeBillingOnlyReportingDetailsAsync() async {
const String __pigeon_channelName =
'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.createAlternativeBillingOnlyReportingDetailsAsync';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList =
await __pigeon_channel.send(null) as List<Object?>?;
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<Object?> pigeonChannelCodec =
_InAppPurchaseCallbackApiCodec();
/// Called for BillingClientStateListener#onBillingServiceDisconnected().
void onBillingServiceDisconnected(int callbackHandle);
/// Called for PurchasesUpdatedListener#onPurchasesUpdated(BillingResult, List<Purchase>).
void onPurchasesUpdated(PlatformPurchasesResponse update);
/// Called for UserChoiceBillingListener#userSelectedAlternativeBilling(UserChoiceDetails).
void userSelectedalternativeBilling(PlatformUserChoiceDetails details);
static void setup(InAppPurchaseCallbackApi? api,
{BinaryMessenger? binaryMessenger}) {
{
final BasicMessageChannel<Object?> __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<Object?> args = (message as List<Object?>?)!;
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<Object?> __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<Object?> args = (message as List<Object?>?)!;
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<Object?> __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<Object?> args = (message as List<Object?>?)!;
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()));
}
});
}
}
}
}

View File

@ -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<Object?, Object?>).cast<String, Object?>()))
.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<Object?, Object?>).cast<String, Object?>()))
.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<Object?, Object?>).cast<String, Object?>()))
.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<Object?, Object?>).cast<String, Object?>()))
.toList(),
);
}

View File

@ -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.

View File

@ -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<String, Object?> 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<Object?> 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<String, Object?> 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<Object?> 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<String, Object?> 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<Object?> 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<String, Object?> 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<Object?> 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<PlatformProduct> 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<Purchase>).
void onPurchasesUpdated(PlatformPurchasesResponse update);
/// Called for UserChoiceBillingListener#userSelectedAlternativeBilling(UserChoiceDetails).
void userSelectedalternativeBilling(PlatformUserChoiceDetails details);
}

View File

@ -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:

View File

@ -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<void> 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<void>.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<void> connectedCompleter = Completer<void>();
when(mockApi.startConnection(any, any)).thenAnswer((_) async {
connectedCompleter.complete();
return PlatformBillingResult(responseCode: 0, debugMessage: '');
});
final Completer<void> calledCompleter1 = Completer<void>();
final Completer<void> calledCompleter2 = Completer<void>();
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,
<String, dynamic>{'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<Object?, Object?> arguments;
stubPlatform.addResponse(
name: startConnectionCall,
additionalStepBeforeReturn: (dynamic value) =>
arguments = value as Map<dynamic, dynamic>,
);
clearInteractions(mockApi);
/// Fake the disconnect that we would expect from a endConnectionCall.
await manager.client.callHandler(
const MethodCall(onBillingServiceDisconnectedCallback,
<String, dynamic>{'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 {});

View File

@ -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<Object>>[MockSpec<InAppPurchaseApi>()])
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: <String, dynamic>{
'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: <String, dynamic>{
'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(<dynamic, dynamic>{
'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: <String, dynamic>{
'responseCode': const BillingResponseConverter().toJson(responseCode),
'debugMessage': debugMessage,
},
);
await billingClient.startConnection(
onBillingServiceDisconnected: () {},
billingChoiceMode: BillingChoiceMode.alternativeBillingOnly);
final MethodCall call = stubPlatform.previousCallMatching(methodName);
expect(
call.arguments,
equals(<dynamic, dynamic>{
'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: <String, dynamic>{
'responseCode': const BillingResponseConverter().toJson(responseCode),
'debugMessage': debugMessage,
},
);
final Completer<UserChoiceDetailsWrapper> completer =
Completer<UserChoiceDetailsWrapper>();
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(<dynamic, dynamic>{
'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: <dynamic, dynamic>{
'billingResult': <String, dynamic>{
'responseCode': const BillingResponseConverter().toJson(responseCode),
'debugMessage': debugMessage,
},
'productDetailsList': <Map<String, dynamic>>[]
});
when(mockApi.queryProductDetailsAsync(any))
.thenAnswer((_) async => PlatformProductDetailsResponse(
billingResult: PlatformBillingResult(
responseCode:
const BillingResponseConverter().toJson(responseCode),
debugMessage: debugMessage),
productDetailsJsonList: <Map<String, dynamic>>[],
));
final ProductDetailsResponseWrapper response = await billingClient
.queryProductDetails(productList: <ProductWrapper>[
@ -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: <String, dynamic>{
'billingResult': <String, dynamic>{
'responseCode': const BillingResponseConverter().toJson(responseCode),
'debugMessage': debugMessage,
},
'productDetailsList': <Map<String, dynamic>>[
when(mockApi.queryProductDetailsAsync(any))
.thenAnswer((_) async => PlatformProductDetailsResponse(
billingResult: PlatformBillingResult(
responseCode:
const BillingResponseConverter().toJson(responseCode),
debugMessage: debugMessage),
productDetailsJsonList: <Map<String, dynamic>>[
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: <ProductWrapper>[
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<dynamic, dynamic> arguments = stubPlatform
.previousCallMatching(launchMethodName)
.arguments as Map<dynamic, dynamic>;
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<dynamic, dynamic> arguments = stubPlatform
.previousCallMatching(launchMethodName)
.arguments as Map<dynamic, dynamic>;
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<dynamic, dynamic> arguments = stubPlatform
.previousCallMatching(launchMethodName)
.arguments as Map<dynamic, dynamic>;
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<dynamic, dynamic> arguments = stubPlatform
.previousCallMatching(launchMethodName)
.arguments as Map<dynamic, dynamic>;
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<dynamic, dynamic> arguments = stubPlatform
.previousCallMatching(launchMethodName)
.arguments as Map<dynamic, dynamic>;
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<PurchaseWrapper> expectedList = <PurchaseWrapper>[
@ -542,14 +436,16 @@ void main() {
const String debugMessage = 'dummy message';
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: expectedCode, debugMessage: debugMessage);
stubPlatform
.addResponse(name: queryPurchasesMethodName, value: <String, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(expectedCode),
'purchasesList': expectedList
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: <String, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(expectedCode),
'purchasesList': <dynamic>[],
});
when(mockApi.queryPurchasesAsync(any))
.thenAnswer((_) async => PlatformPurchasesResponse(
billingResult: PlatformBillingResult(
responseCode:
const BillingResponseConverter().toJson(expectedCode),
debugMessage: debugMessage),
purchasesJsonList: <Map<String, dynamic>>[],
));
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<PurchaseHistoryRecordWrapper> 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: <String, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'purchaseHistoryRecordList': expectedList
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: <dynamic, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'purchaseHistoryRecordList': <dynamic>[],
});
when(mockApi.queryPurchaseHistoryAsync(any))
.thenAnswer((_) async => PlatformPurchaseHistoryResponse(
billingResult: PlatformBillingResult(
responseCode:
const BillingResponseConverter().toJson(expectedCode),
debugMessage: debugMessage),
purchaseHistoryRecordJsonList: <Map<String, dynamic>>[],
));
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<Object?, Object?> arguments;
stubPlatform.addResponse(
name: isFeatureSupportedMethodName,
value: false,
additionalStepBeforeReturn: (dynamic value) =>
arguments = value as Map<dynamic, dynamic>,
);
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<Object?, Object?> arguments;
stubPlatform.addResponse(
name: isFeatureSupportedMethodName,
value: true,
additionalStepBeforeReturn: (dynamic value) =>
arguments = value as Map<dynamic, dynamic>,
);
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<String, dynamic> buildBillingConfigMap(BillingConfigWrapper original) {
return <String, dynamic>{
'responseCode':
PlatformBillingConfigResponse platformBillingConfigFromWrapper(
BillingConfigWrapper original) {
return PlatformBillingConfigResponse(
billingResult: PlatformBillingResult(
responseCode:
const BillingResponseConverter().toJson(original.responseCode),
'debugMessage': original.debugMessage,
'countryCode': original.countryCode,
};
debugMessage: original.debugMessage!,
),
countryCode: original.countryCode);
}
Map<String, dynamic> buildAlternativeBillingOnlyReportingDetailsMap(
PlatformAlternativeBillingOnlyReportingDetailsResponse
platformAlternativeBillingOnlyReportingDetailsFromWrapper(
AlternativeBillingOnlyReportingDetailsWrapper original) {
return <String, dynamic>{
'responseCode':
return PlatformAlternativeBillingOnlyReportingDetailsResponse(
billingResult: PlatformBillingResult(
responseCode:
const BillingResponseConverter().toJson(original.responseCode),
'debugMessage': original.debugMessage,
// from: io/flutter/plugins/inapppurchase/Translator.java
'externalTransactionToken': original.externalTransactionToken,
};
debugMessage: original.debugMessage!,
),
externalTransactionToken: original.externalTransactionToken);
}

View File

@ -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<bool> isReady() => (super.noSuchMethod(
Invocation.method(
#isReady,
[],
),
returnValue: _i3.Future<bool>.value(false),
returnValueForMissingStub: _i3.Future<bool>.value(false),
) as _i3.Future<bool>);
@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<void> endConnection() => (super.noSuchMethod(
Invocation.method(
#endConnection,
[],
),
returnValue: _i3.Future<void>.value(),
returnValueForMissingStub: _i3.Future<void>.value(),
) as _i3.Future<void>);
@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<bool> isFeatureSupported(String? feature) => (super.noSuchMethod(
Invocation.method(
#isFeatureSupported,
[feature],
),
returnValue: _i3.Future<bool>.value(false),
returnValueForMissingStub: _i3.Future<bool>.value(false),
) as _i3.Future<bool>);
@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>);
}

View File

@ -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<Object?, Object?> arguments;
test('setAlternativeBillingOnlyState', () async {
stubPlatform.reset();
stubPlatform.addResponse(
name: startConnectionCall,
additionalStepBeforeReturn: (dynamic value) =>
arguments = value as Map<dynamic, dynamic>,
);
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,
<String, dynamic>{'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<dynamic, dynamic>,
);
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,
<String, dynamic>{'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: <dynamic, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(responseCode),
'purchasesList': <Map<String, dynamic>>[]
});
final QueryPurchaseDetailsResponse response =
await iapAndroidPlatformAddition.queryPastPurchases();
expect(response.pastPurchases, isEmpty);
expect(response.error, isNotNull);
expect(
response.error!.message, BillingResponse.developerError.toString());
expect(response.error!.source, kIAPSource);
});
test('returns 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: <String, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(responseCode),
'purchasesList': <Map<String, dynamic>>[
when(mockApi.queryPurchasesAsync(any))
.thenAnswer((_) async => PlatformPurchasesResponse(
billingResult: PlatformBillingResult(
responseCode:
const BillingResponseConverter().toJson(responseCode),
debugMessage: debugMessage),
purchasesJsonList: <Map<String, dynamic>>[
buildPurchaseMap(dummyPurchase),
]
});
],
));
// Since queryPastPurchases makes 2 platform method calls (one for each ProductType), the result will contain 2 dummyWrapper instead
// of 1.
@ -219,20 +158,7 @@ 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: <dynamic, dynamic>{
'responseCode':
const BillingResponseConverter().toJson(responseCode),
'billingResult': buildBillingResultMap(expectedBillingResult),
'purchasesList': <Map<String, dynamic>>[]
},
additionalStepBeforeReturn: (dynamic _) {
when(mockApi.queryPurchasesAsync(any)).thenAnswer((_) async {
throw PlatformException(
code: 'error_code',
message: 'error_message',
@ -252,34 +178,20 @@ void main() {
});
group('isFeatureSupported', () {
const String isFeatureSupportedMethodName =
'BillingClient#isFeatureSupported(String)';
test('isFeatureSupported returns false', () async {
late Map<Object?, Object?> arguments;
stubPlatform.addResponse(
name: isFeatureSupportedMethodName,
value: false,
additionalStepBeforeReturn: (dynamic value) =>
arguments = value as Map<dynamic, dynamic>,
);
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<Object?, Object?> arguments;
stubPlatform.addResponse(
name: isFeatureSupportedMethodName,
value: true,
additionalStepBeforeReturn: (dynamic value) =>
arguments = value as Map<dynamic, dynamic>,
);
when(mockApi.isFeatureSupported('subscriptions'))
.thenAnswer((_) async => true);
final bool isSupported = await iapAndroidPlatformAddition
.isFeatureSupported(BillingClientFeature.subscriptions);
expect(isSupported, isTrue);
expect(arguments['feature'], equals('subscriptions'));
});
});

View File

@ -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,
<String, dynamic>{'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<String, dynamic> okValue = buildBillingResultMap(
const BillingResultWrapper(responseCode: BillingResponse.ok));
stubPlatform.addResponse(
name: acknowledgePurchaseCall,
value: buildBillingResultMap(
const BillingResultWrapper(
responseCode: BillingResponse.serviceDisconnected,
),
),
when(mockApi.acknowledgePurchase(any)).thenAnswer(
(_) async => PlatformBillingResult(
responseCode: const BillingResponseConverter()
.toJson(BillingResponse.serviceDisconnected),
debugMessage: 'disconnected'),
);
stubPlatform.addResponse(
name: startConnectionCall,
value: okValue,
additionalStepBeforeReturn: (dynamic _) => stubPlatform.addResponse(
name: acknowledgePurchaseCall, value: okValue),
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: <String, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'productDetailsList': <Map<String, dynamic>>[],
});
when(mockApi.queryProductDetailsAsync(any))
.thenAnswer((_) async => PlatformProductDetailsResponse(
billingResult: PlatformBillingResult(
responseCode:
const BillingResponseConverter().toJson(responseCode),
debugMessage: debugMessage),
productDetailsJsonList: <Map<String, dynamic>>[],
));
final ProductDetailsResponse response =
await iapAndroidPlatform.queryProductDetails(<String>{''});
@ -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: <String, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'productDetailsList': <Map<String, dynamic>>[
when(mockApi.queryProductDetailsAsync(any))
.thenAnswer((_) async => PlatformProductDetailsResponse(
billingResult: PlatformBillingResult(
responseCode:
const BillingResponseConverter().toJson(responseCode),
debugMessage: debugMessage),
productDetailsJsonList: <Map<String, dynamic>>[
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: <String, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'productDetailsList': <Map<String, dynamic>>[
when(mockApi.queryProductDetailsAsync(any))
.thenAnswer((_) async => PlatformProductDetailsResponse(
billingResult: PlatformBillingResult(
responseCode:
const BillingResponseConverter().toJson(responseCode),
debugMessage: debugMessage),
productDetailsJsonList: <Map<String, dynamic>>[
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,17 +163,7 @@ 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: <String, dynamic>{
'responseCode':
const BillingResponseConverter().toJson(responseCode),
'productDetailsList': <Map<String, dynamic>>[
buildProductMap(dummyOneTimeProductDetails)
]
},
additionalStepBeforeReturn: (dynamic _) {
when(mockApi.queryProductDetailsAsync(any)).thenAnswer((_) async {
throw PlatformException(
code: 'error_code',
message: 'error_message',
@ -212,49 +185,8 @@ 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: <dynamic, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(responseCode),
'purchasesList': <Map<String, dynamic>>[]
});
expect(
iapAndroidPlatform.restorePurchases(),
throwsA(
isA<InAppPurchaseException>()
.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: <dynamic, dynamic>{
'responseCode':
const BillingResponseConverter().toJson(responseCode),
'billingResult': buildBillingResultMap(expectedBillingResult),
'purchasesList': <Map<String, dynamic>>[]
},
additionalStepBeforeReturn: (dynamic _) {
when(mockApi.queryPurchasesAsync(any)).thenAnswer((_) async {
throw PlatformException(
code: 'error_code',
message: 'error_message',
@ -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: <String, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(responseCode),
'purchasesList': <Map<String, dynamic>>[
when(mockApi.queryPurchasesAsync(any))
.thenAnswer((_) async => PlatformPurchasesResponse(
billingResult: PlatformBillingResult(
responseCode:
const BillingResponseConverter().toJson(responseCode),
debugMessage: debugMessage),
purchasesJsonList: <Map<String, dynamic>>[
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,16 +269,12 @@ void main() {
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: sentCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: launchMethodName,
value: buildBillingResultMap(expectedBillingResult),
additionalStepBeforeReturn: (dynamic _) {
when(mockApi.launchBillingFlow(any)).thenAnswer((_) async {
// Mock java update purchase callback.
final MethodCall call =
MethodCall(kOnPurchasesUpdated, <dynamic, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(sentCode),
'purchasesList': <dynamic>[
iapAndroidPlatform.billingClientManager.client.hostCallbackHandler
.onPurchasesUpdated(PlatformPurchasesResponse(
billingResult: convertToPigeonResult(expectedBillingResult),
purchasesJsonList: <Object?>[
<dynamic, dynamic>{
'orderId': 'orderID1',
'products': <String>[productDetails.productId],
@ -364,9 +288,10 @@ void main() {
'isAcknowledged': true,
'purchaseState': 1,
}
]
});
iapAndroidPlatform.billingClientManager.client.callHandler(call);
],
));
return convertToPigeonResult(expectedBillingResult);
});
final Completer<PurchaseDetails> completer = Completer<PurchaseDetails>();
PurchaseDetails purchaseDetails;
@ -400,18 +325,15 @@ void main() {
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: sentCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: launchMethodName,
value: buildBillingResultMap(expectedBillingResult),
additionalStepBeforeReturn: (dynamic _) {
when(mockApi.launchBillingFlow(any)).thenAnswer((_) async {
// Mock java update purchase callback.
final MethodCall call =
MethodCall(kOnPurchasesUpdated, <dynamic, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(sentCode),
'purchasesList': const <dynamic>[]
});
iapAndroidPlatform.billingClientManager.client.callHandler(call);
iapAndroidPlatform.billingClientManager.client.hostCallbackHandler
.onPurchasesUpdated(PlatformPurchasesResponse(
billingResult: convertToPigeonResult(expectedBillingResult),
purchasesJsonList: <Object?>[],
));
return convertToPigeonResult(expectedBillingResult);
});
final Completer<PurchaseDetails> completer = Completer<PurchaseDetails>();
PurchaseDetails purchaseDetails;
@ -445,16 +367,12 @@ void main() {
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: sentCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: launchMethodName,
value: buildBillingResultMap(expectedBillingResult),
additionalStepBeforeReturn: (dynamic _) {
when(mockApi.launchBillingFlow(any)).thenAnswer((_) async {
// Mock java update purchase callback.
final MethodCall call =
MethodCall(kOnPurchasesUpdated, <dynamic, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(sentCode),
'purchasesList': <dynamic>[
iapAndroidPlatform.billingClientManager.client.hostCallbackHandler
.onPurchasesUpdated(PlatformPurchasesResponse(
billingResult: convertToPigeonResult(expectedBillingResult),
purchasesJsonList: <Object?>[
<dynamic, dynamic>{
'orderId': 'orderID1',
'products': <String>[productDetails.productId],
@ -468,9 +386,10 @@ void main() {
'isAcknowledged': true,
'purchaseState': 1,
}
]
});
iapAndroidPlatform.billingClientManager.client.callHandler(call);
],
));
return convertToPigeonResult(expectedBillingResult);
});
final Completer<String> consumeCompleter = Completer<String>();
// adding call back for consume purchase
@ -478,13 +397,11 @@ void main() {
const BillingResultWrapper expectedBillingResultForConsume =
BillingResultWrapper(
responseCode: expectedCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: consumeMethodName,
value: buildBillingResultMap(expectedBillingResultForConsume),
additionalStepBeforeReturn: (dynamic args) {
when(mockApi.consumeAsync(any)).thenAnswer((Invocation invocation) async {
final String purchaseToken =
(args as Map<Object?, Object?>)['purchaseToken']! as String;
invocation.positionalArguments.first as String;
consumeCompleter.complete(purchaseToken);
return convertToPigeonResult(expectedBillingResultForConsume);
});
final Completer<PurchaseDetails> completer = Completer<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,16 +477,12 @@ void main() {
const BillingResponse sentCode = BillingResponse.ok;
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: sentCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: launchMethodName,
value: buildBillingResultMap(expectedBillingResult),
additionalStepBeforeReturn: (dynamic _) {
when(mockApi.launchBillingFlow(any)).thenAnswer((_) async {
// Mock java update purchase callback.
final MethodCall call =
MethodCall(kOnPurchasesUpdated, <dynamic, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(sentCode),
'purchasesList': <dynamic>[
iapAndroidPlatform.billingClientManager.client.hostCallbackHandler
.onPurchasesUpdated(PlatformPurchasesResponse(
billingResult: convertToPigeonResult(expectedBillingResult),
purchasesJsonList: <Object?>[
<dynamic, dynamic>{
'orderId': 'orderID1',
'products': <String>[productDetails.productId],
@ -586,9 +496,10 @@ void main() {
'isAcknowledged': true,
'purchaseState': 1,
}
]
});
iapAndroidPlatform.billingClientManager.client.callHandler(call);
],
));
return convertToPigeonResult(expectedBillingResult);
});
final Completer<String> consumeCompleter = Completer<String>();
// adding call back for consume purchase
@ -596,13 +507,11 @@ void main() {
const BillingResultWrapper expectedBillingResultForConsume =
BillingResultWrapper(
responseCode: expectedCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: consumeMethodName,
value: buildBillingResultMap(expectedBillingResultForConsume),
additionalStepBeforeReturn: (dynamic args) {
when(mockApi.consumeAsync(any)).thenAnswer((Invocation invocation) async {
final String purchaseToken =
(args as Map<Object?, Object?>)['purchaseToken']! as String;
invocation.positionalArguments.first as String;
consumeCompleter.complete(purchaseToken);
return convertToPigeonResult(expectedBillingResultForConsume);
});
final Completer<PurchaseDetails> completer = Completer<PurchaseDetails>();
@ -642,16 +551,12 @@ void main() {
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: sentCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: launchMethodName,
value: buildBillingResultMap(expectedBillingResult),
additionalStepBeforeReturn: (dynamic _) {
when(mockApi.launchBillingFlow(any)).thenAnswer((_) async {
// Mock java update purchase callback.
final MethodCall call =
MethodCall(kOnPurchasesUpdated, <dynamic, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(sentCode),
'purchasesList': <dynamic>[
iapAndroidPlatform.billingClientManager.client.hostCallbackHandler
.onPurchasesUpdated(PlatformPurchasesResponse(
billingResult: convertToPigeonResult(expectedBillingResult),
purchasesJsonList: <Object?>[
<dynamic, dynamic>{
'orderId': 'orderID1',
'products': <String>[productDetails.productId],
@ -665,9 +570,10 @@ void main() {
'isAcknowledged': true,
'purchaseState': 1,
}
]
});
iapAndroidPlatform.billingClientManager.client.callHandler(call);
],
));
return convertToPigeonResult(expectedBillingResult);
});
final Completer<String?> consumeCompleter = Completer<String?>();
// adding call back for consume purchase
@ -675,13 +581,11 @@ void main() {
const BillingResultWrapper expectedBillingResultForConsume =
BillingResultWrapper(
responseCode: expectedCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: consumeMethodName,
value: buildBillingResultMap(expectedBillingResultForConsume),
additionalStepBeforeReturn: (dynamic args) {
when(mockApi.consumeAsync(any)).thenAnswer((Invocation invocation) async {
final String purchaseToken =
(args as Map<Object?, Object?>)['purchaseToken']! as String;
invocation.positionalArguments.first as String;
consumeCompleter.complete(purchaseToken);
return convertToPigeonResult(expectedBillingResultForConsume);
});
final Stream<List<PurchaseDetails>> purchaseStream =
@ -709,16 +613,12 @@ void main() {
const BillingResponse sentCode = BillingResponse.userCanceled;
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: sentCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: launchMethodName,
value: buildBillingResultMap(expectedBillingResult),
additionalStepBeforeReturn: (dynamic _) {
when(mockApi.launchBillingFlow(any)).thenAnswer((_) async {
// Mock java update purchase callback.
final MethodCall call =
MethodCall(kOnPurchasesUpdated, <dynamic, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(sentCode),
'purchasesList': <dynamic>[
iapAndroidPlatform.billingClientManager.client.hostCallbackHandler
.onPurchasesUpdated(PlatformPurchasesResponse(
billingResult: convertToPigeonResult(expectedBillingResult),
purchasesJsonList: <Object?>[
<dynamic, dynamic>{
'orderId': 'orderID1',
'products': <String>[productDetails.productId],
@ -732,9 +632,10 @@ void main() {
'isAcknowledged': true,
'purchaseState': 1,
}
]
});
iapAndroidPlatform.billingClientManager.client.callHandler(call);
],
));
return convertToPigeonResult(expectedBillingResult);
});
final Completer<String> consumeCompleter = Completer<String>();
// adding call back for consume purchase
@ -742,13 +643,11 @@ void main() {
const BillingResultWrapper expectedBillingResultForConsume =
BillingResultWrapper(
responseCode: expectedCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: consumeMethodName,
value: buildBillingResultMap(expectedBillingResultForConsume),
additionalStepBeforeReturn: (dynamic args) {
when(mockApi.consumeAsync(any)).thenAnswer((Invocation invocation) async {
final String purchaseToken =
(args as Map<Object?, Object?>)['purchaseToken']! as String;
invocation.positionalArguments.first as String;
consumeCompleter.complete(purchaseToken);
return convertToPigeonResult(expectedBillingResultForConsume);
});
final Completer<PurchaseDetails> completer = Completer<PurchaseDetails>();
@ -782,18 +681,15 @@ void main() {
const BillingResponse sentCode = BillingResponse.ok;
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: sentCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: launchMethodName,
value: buildBillingResultMap(expectedBillingResult),
additionalStepBeforeReturn: (dynamic _) {
when(mockApi.launchBillingFlow(any)).thenAnswer((_) async {
// Mock java update purchase callback.
final MethodCall call =
MethodCall(kOnPurchasesUpdated, <dynamic, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(sentCode),
'purchasesList': const <dynamic>[]
});
iapAndroidPlatform.billingClientManager.client.callHandler(call);
iapAndroidPlatform.billingClientManager.client.hostCallbackHandler
.onPurchasesUpdated(PlatformPurchasesResponse(
billingResult: convertToPigeonResult(expectedBillingResult),
purchasesJsonList: <Object?>[],
));
return convertToPigeonResult(expectedBillingResult);
});
final Completer<PurchaseDetails> completer = Completer<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;

View File

@ -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<dynamic>` instead of `FutureOr<void>` to avoid
// "don't assign to void" warnings.
typedef AdditionalSteps = FutureOr<dynamic> Function(dynamic args);
class StubInAppPurchasePlatform {
final Map<String, dynamic> _expectedCalls = <String, dynamic>{};
final Map<String, AdditionalSteps?> _additionalSteps =
<String, AdditionalSteps?>{};
void addResponse(
{required String name,
dynamic value,
AdditionalSteps? additionalStepBeforeReturn}) {
_additionalSteps[name] = additionalStepBeforeReturn;
_expectedCalls[name] = value;
}
final List<MethodCall> _previousCalls = <MethodCall>[];
List<MethodCall> 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<dynamic> 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<dynamic>.sync(() => _expectedCalls[call.method]);
} else {
return Future<void>.sync(() => null);
}
}
}

View File

@ -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!,
);
}