Skip to content

Commit

Permalink
[in_app_purchase] Add alternative billing apis for android (flutter#6056
Browse files Browse the repository at this point in the history
)

- Update the emulator versions and expose cipd. (flutter#6025
- Enable alternitive billing only available check, add test and code to handle service unavilable in getBillingConfig
- Enable alternative billing only during client creation and tests covering fallback path
- ShowAlternativeBillingDialog android native method added
- Add tests for null activity behavior
- Remove not needed lines of code
- Add showAlternativeBillingOnlyInformationDialog and isAlternativeBillingOnlyAvailable to android platform addition and billing client wrapper.
- test showAlternativeBillingOnlyInformationDialog and isAlternativeBillingOnlyAvailable in platfrom addition and billing_client

Fixes flutter/flutter/issues/142618

Still left TODO: 
* [x] incorporate new apis into example app 
* [x] expose alternative billing only [dart api](https://github.com/flutter/packages/pull/6056/files/d4c445422f2cd3f0627f575d85b59b559e0e9f69#r1480455450)
* [x] Expose alternative billing reporting details
* [ ] Configure end to end working example with playstore
  • Loading branch information
reidbaker authored and arc-yong committed Jun 14, 2024
1 parent 9da32c3 commit ac11e77
Show file tree
Hide file tree
Showing 23 changed files with 996 additions and 45 deletions.
4 changes: 4 additions & 0 deletions packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.3.1

* Adds alternative-billing-only APIs to InAppPurchaseAndroidPlatformAddition.

## 0.3.0+18

* Adds new getCountryCode() method to InAppPurchaseAndroidPlatformAddition to get a customer's country code.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,6 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20231013'
testImplementation 'org.mockito:mockito-core:5.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ interface BillingClientFactory {
*
* @param context The context used to create the {@link BillingClient}.
* @param channel The method channel used to create 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);
BillingClient createBillingClient(
@NonNull Context context, @NonNull MethodChannel channel, int billingChoiceMode);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@
import androidx.annotation.NonNull;
import com.android.billingclient.api.BillingClient;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.BillingChoiceMode;

/** The implementation for {@link BillingClientFactory} for the plugin. */
final class BillingClientFactoryImpl implements BillingClientFactory {

@Override
public BillingClient createBillingClient(
@NonNull Context context, @NonNull MethodChannel channel) {
@NonNull Context context, @NonNull MethodChannel channel, int billingChoiceMode) {
BillingClient.Builder builder = BillingClient.newBuilder(context).enablePendingPurchases();

if (billingChoiceMode == BillingChoiceMode.ALTERNATIVE_BILLING_ONLY) {
// https://developer.android.com/google/play/billing/alternative/alternative-billing-without-user-choice-in-app
builder.enableAlternativeBillingOnly();
}
return builder.setListener(new PluginPurchaseListener(channel)).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package io.flutter.plugins.inapppurchase;

import static io.flutter.plugins.inapppurchase.Translator.fromAlternativeBillingOnlyReportingDetails;
import static io.flutter.plugins.inapppurchase.Translator.fromBillingConfig;
import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult;
import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList;
Expand Down Expand Up @@ -65,10 +66,36 @@ static final class MethodNames {
static final String IS_FEATURE_SUPPORTED = "BillingClient#isFeatureSupported(String)";
static final String GET_CONNECTION_STATE = "BillingClient#getConnectionState()";
static final String GET_BILLING_CONFIG = "BillingClient#getBillingConfig()";
static final String IS_ALTERNATIVE_BILLING_ONLY_AVAILABLE =
"BillingClient#isAlternativeBillingOnlyAvailable()";
static final String CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS =
"BillingClient#createAlternativeBillingOnlyReportingDetails()";
static final String SHOW_ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG =
"BillingClient#showAlternativeBillingOnlyInformationDialog()";

private MethodNames() {}
}

@VisibleForTesting
static final class MethodArgs {

// Key for an int argument passed into startConnection
static final String HANDLE = "handle";
// Key for a boolean argument passed into startConnection.
static final String BILLING_CHOICE_MODE = "billingChoiceMode";

private MethodArgs() {}
}

/**
* Values here must match values used in
* in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart
*/
static final class BillingChoiceMode {
static final int PLAY_BILLING_ONLY = 0;
static final int ALTERNATIVE_BILLING_ONLY = 1;
}

// TODO(gmackall): Replace uses of deprecated ProrationMode enum values with new
// ReplacementMode enum values.
// https://github.com/flutter/flutter/issues/128957.
Expand All @@ -80,6 +107,7 @@ private MethodNames() {}
private static final String TAG = "InAppPurchasePlugin";
private static final String LOAD_PRODUCT_DOC_URL =
"https://github.com/flutter/packages/blob/main/packages/in_app_purchase/in_app_purchase/README.md#loading-products-for-sale";
@VisibleForTesting static final String ACTIVITY_UNAVAILABLE = "ACTIVITY_UNAVAILABLE";

@Nullable private BillingClient billingClient;
private final BillingClientFactory billingClientFactory;
Expand Down Expand Up @@ -147,7 +175,12 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result
isReady(result);
break;
case MethodNames.START_CONNECTION:
startConnection((int) call.argument("handle"), result);
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);
Expand Down Expand Up @@ -190,12 +223,61 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result
case MethodNames.GET_BILLING_CONFIG:
getBillingConfig(result);
break;
case MethodNames.IS_ALTERNATIVE_BILLING_ONLY_AVAILABLE:
isAlternativeBillingOnlyAvailable(result);
break;
case MethodNames.CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS:
createAlternativeBillingOnlyReportingDetails(result);
break;
case MethodNames.SHOW_ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG:
showAlternativeBillingOnlyInformationDialog(result);
break;
default:
result.notImplemented();
}
}

private void showAlternativeBillingOnlyInformationDialog(final MethodChannel.Result result) {
if (billingClientError(result)) {
return;
}
if (activity == null) {
result.error(ACTIVITY_UNAVAILABLE, "Not attempting to show dialog", null);
return;
}
billingClient.showAlternativeBillingOnlyInformationDialog(
activity,
billingResult -> {
result.success(fromBillingResult(billingResult));
});
}

private void createAlternativeBillingOnlyReportingDetails(final MethodChannel.Result result) {
if (billingClientError(result)) {
return;
}
billingClient.createAlternativeBillingOnlyReportingDetailsAsync(
((billingResult, alternativeBillingOnlyReportingDetails) -> {
result.success(
fromAlternativeBillingOnlyReportingDetails(
billingResult, alternativeBillingOnlyReportingDetails));
}));
}

private void isAlternativeBillingOnlyAvailable(final MethodChannel.Result result) {
if (billingClientError(result)) {
return;
}
billingClient.isAlternativeBillingOnlyAvailableAsync(
billingResult -> {
result.success(fromBillingResult(billingResult));
});
}

private void getBillingConfig(final MethodChannel.Result result) {
if (billingClientError(result)) {
return;
}
billingClient.getBillingConfigAsync(
GetBillingConfigParams.newBuilder().build(),
(billingResult, billingConfig) -> {
Expand Down Expand Up @@ -313,7 +395,7 @@ private void launchBillingFlow(

if (activity == null) {
result.error(
"ACTIVITY_UNAVAILABLE",
ACTIVITY_UNAVAILABLE,
"Details for product "
+ product
+ " are not available. This method must be run with the app in foreground.",
Expand Down Expand Up @@ -422,9 +504,12 @@ private void getConnectionState(final MethodChannel.Result result) {
result.success(serialized);
}

private void startConnection(final int handle, final MethodChannel.Result result) {
private void startConnection(
final int handle, final MethodChannel.Result result, int billingChoiceMode) {
if (billingClient == null) {
billingClient = billingClientFactory.createBillingClient(applicationContext, methodChannel);
billingClient =
billingClientFactory.createBillingClient(
applicationContext, methodChannel, billingChoiceMode);
}

billingClient.startConnection(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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.BillingConfig;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ProductDetails;
Expand Down Expand Up @@ -240,6 +241,18 @@ static HashMap<String, Object> fromBillingConfig(
return info;
}

/**
* Converter from {@link BillingResult} and {@link AlternativeBillingOnlyReportingDetails} to map.
*/
static HashMap<String, Object> fromAlternativeBillingOnlyReportingDetails(
BillingResult result, AlternativeBillingOnlyReportingDetails details) {
HashMap<String, Object> info = fromBillingResult(result);
if (details != null) {
info.put("externalTransactionToken", details.getExternalTransactionToken());
}
return info;
}

/**
* Gets the symbol of for the given currency code for the default {@link Locale.Category#DISPLAY
* DISPLAY} locale. For example, for the US Dollar, the symbol is "$" if the default locale is the
Expand Down
Loading

0 comments on commit ac11e77

Please sign in to comment.