Skip to content

[in_app_purchase] Add alternative billing apis for android #6056

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
953338a
Enable alternitive billing only available check, add test and code to…
reidbaker Feb 2, 2024
8ab32cc
Enable alternative billing only during client creation and tests cove…
reidbaker Feb 2, 2024
4d06253
ShowAlternativeBillingDialog android native method added
reidbaker Feb 2, 2024
8d95e87
Add tests for null activity behavior
reidbaker Feb 2, 2024
3878b4f
Remove not needed lines of code
reidbaker Feb 2, 2024
e516655
Add showAlternativeBillingOnlyInformationDialog and isAlternativeBill…
reidbaker Feb 5, 2024
c8efd99
test showAlternativeBillingOnlyInformationDialog and isAlternativeBil…
reidbaker Feb 5, 2024
3bd6a95
add alternative billing only reporting details implementation and tests
reidbaker Feb 5, 2024
3e77c14
formatting and generated code
reidbaker Feb 5, 2024
b77152b
Changelog and version bump added
reidbaker Feb 5, 2024
8cacaef
Formatting
reidbaker Feb 5, 2024
d4c4454
Merge branch 'main' into i142618-alternitive-billing-api-intro
reidbaker Feb 6, 2024
2f91615
Update to api 34 (required to test end to end, update dependencies, f…
reidbaker Feb 6, 2024
fe7a677
Use 0.3.1 instead of 0+19
reidbaker Feb 6, 2024
95214de
Expose country code as labled button
reidbaker Feb 7, 2024
fd506c2
Expose other alternative apis as buttons in their own section
reidbaker Feb 7, 2024
6415bee
formatting
reidbaker Feb 7, 2024
448dd2c
Add test for BillingClientManager alternative billing only
reidbaker Feb 8, 2024
1524fe0
Add platform addition tests
reidbaker Feb 8, 2024
7507f02
Show the dialog result code
reidbaker Feb 8, 2024
2ae9119
Set alternative billing only to true in example app
reidbaker Feb 8, 2024
99b3baf
Speling mistake fix
reidbaker Feb 9, 2024
b72cd39
Merge branch 'main' into i142618-alternitive-billing-api-intro
reidbaker Feb 9, 2024
240df4a
revert dependencies since they conflict with integration_test
reidbaker Feb 9, 2024
0723af9
changelog copy change
reidbaker Feb 9, 2024
16ce293
Migrate to enum for alternative billing only and play billing
reidbaker Feb 9, 2024
dfbb6b7
java formatting
reidbaker Feb 9, 2024
d073552
Include generated code change
reidbaker Feb 9, 2024
8e041af
Use json conversion for sending object over wire
reidbaker Feb 9, 2024
fe9d77e
Change button text
reidbaker Feb 9, 2024
31788de
Merge branch 'main' into i142618-alternitive-billing-api-intro
reidbaker Feb 9, 2024
31008fa
billingChoiceMode instead of enableAlternatitveBillingOnly
reidbaker Feb 9, 2024
b59c645
billing choice converstion to java
reidbaker Feb 10, 2024
7b6b7a2
Code review feedback, documentation, formatting, spelling
reidbaker Feb 12, 2024
b144067
Add alternative billing only reporting details example code
reidbaker Feb 12, 2024
8450aaa
Changlog formatting
reidbaker Feb 12, 2024
9730db3
Merge branch 'main' into i142618-alternitive-billing-api-intro
reidbaker Feb 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
Copy link
Member

@gmackall gmackall Feb 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not an actionable comment, but: Interesting that the billing client builder example they show at the link doesn't call enablePendingPurchases. That must be a mistake in their documentation, right? It is documented as required in all circumstances
https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enablePendingPurchases()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH I am not sure. Organizationally I think it should be safe to include because if the caller was going to use alternative billing only then there are no pending purchases to enable or if there are all that happens on the callers side since they are providing the billing implementation.

}
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