Skip to content

Commit 41a0078

Browse files
authored
[in_app_purchase] Add alternative billing apis for android (flutter#6056)
- 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/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
1 parent 6828aaa commit 41a0078

23 files changed

+996
-45
lines changed

packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.3.1
2+
3+
* Adds alternative-billing-only APIs to InAppPurchaseAndroidPlatformAddition.
4+
15
## 0.3.0+18
26

37
* Adds new getCountryCode() method to InAppPurchaseAndroidPlatformAddition to get a customer's country code.

packages/in_app_purchase/in_app_purchase_android/android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,6 @@ dependencies {
6666
testImplementation 'junit:junit:4.13.2'
6767
testImplementation 'org.json:json:20231013'
6868
testImplementation 'org.mockito:mockito-core:5.4.0'
69-
androidTestImplementation 'androidx.test:runner:1.4.0'
69+
androidTestImplementation 'androidx.test:runner:1.5.2'
7070
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
7171
}

packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ interface BillingClientFactory {
1717
*
1818
* @param context The context used to create the {@link BillingClient}.
1919
* @param channel The method channel used to create the {@link BillingClient}.
20+
* @param billingChoiceMode Enables the ability to offer alternative billing or Google Play
21+
* billing.
2022
* @return The {@link BillingClient} object that is created.
2123
*/
22-
BillingClient createBillingClient(@NonNull Context context, @NonNull MethodChannel channel);
24+
BillingClient createBillingClient(
25+
@NonNull Context context, @NonNull MethodChannel channel, int billingChoiceMode);
2326
}

packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,19 @@
88
import androidx.annotation.NonNull;
99
import com.android.billingclient.api.BillingClient;
1010
import io.flutter.plugin.common.MethodChannel;
11+
import io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.BillingChoiceMode;
1112

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

1516
@Override
1617
public BillingClient createBillingClient(
17-
@NonNull Context context, @NonNull MethodChannel channel) {
18+
@NonNull Context context, @NonNull MethodChannel channel, int billingChoiceMode) {
1819
BillingClient.Builder builder = BillingClient.newBuilder(context).enablePendingPurchases();
19-
20+
if (billingChoiceMode == BillingChoiceMode.ALTERNATIVE_BILLING_ONLY) {
21+
// https://developer.android.com/google/play/billing/alternative/alternative-billing-without-user-choice-in-app
22+
builder.enableAlternativeBillingOnly();
23+
}
2024
return builder.setListener(new PluginPurchaseListener(channel)).build();
2125
}
2226
}

packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package io.flutter.plugins.inapppurchase;
66

7+
import static io.flutter.plugins.inapppurchase.Translator.fromAlternativeBillingOnlyReportingDetails;
78
import static io.flutter.plugins.inapppurchase.Translator.fromBillingConfig;
89
import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult;
910
import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList;
@@ -65,10 +66,36 @@ static final class MethodNames {
6566
static final String IS_FEATURE_SUPPORTED = "BillingClient#isFeatureSupported(String)";
6667
static final String GET_CONNECTION_STATE = "BillingClient#getConnectionState()";
6768
static final String GET_BILLING_CONFIG = "BillingClient#getBillingConfig()";
69+
static final String IS_ALTERNATIVE_BILLING_ONLY_AVAILABLE =
70+
"BillingClient#isAlternativeBillingOnlyAvailable()";
71+
static final String CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS =
72+
"BillingClient#createAlternativeBillingOnlyReportingDetails()";
73+
static final String SHOW_ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG =
74+
"BillingClient#showAlternativeBillingOnlyInformationDialog()";
6875

6976
private MethodNames() {}
7077
}
7178

79+
@VisibleForTesting
80+
static final class MethodArgs {
81+
82+
// Key for an int argument passed into startConnection
83+
static final String HANDLE = "handle";
84+
// Key for a boolean argument passed into startConnection.
85+
static final String BILLING_CHOICE_MODE = "billingChoiceMode";
86+
87+
private MethodArgs() {}
88+
}
89+
90+
/**
91+
* Values here must match values used in
92+
* in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart
93+
*/
94+
static final class BillingChoiceMode {
95+
static final int PLAY_BILLING_ONLY = 0;
96+
static final int ALTERNATIVE_BILLING_ONLY = 1;
97+
}
98+
7299
// TODO(gmackall): Replace uses of deprecated ProrationMode enum values with new
73100
// ReplacementMode enum values.
74101
// https://github.com/flutter/flutter/issues/128957.
@@ -80,6 +107,7 @@ private MethodNames() {}
80107
private static final String TAG = "InAppPurchasePlugin";
81108
private static final String LOAD_PRODUCT_DOC_URL =
82109
"https://github.com/flutter/packages/blob/main/packages/in_app_purchase/in_app_purchase/README.md#loading-products-for-sale";
110+
@VisibleForTesting static final String ACTIVITY_UNAVAILABLE = "ACTIVITY_UNAVAILABLE";
83111

84112
@Nullable private BillingClient billingClient;
85113
private final BillingClientFactory billingClientFactory;
@@ -147,7 +175,12 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result
147175
isReady(result);
148176
break;
149177
case MethodNames.START_CONNECTION:
150-
startConnection((int) call.argument("handle"), result);
178+
final int handle = (int) call.argument(MethodArgs.HANDLE);
179+
int billingChoiceMode = BillingChoiceMode.PLAY_BILLING_ONLY;
180+
if (call.hasArgument(MethodArgs.BILLING_CHOICE_MODE)) {
181+
billingChoiceMode = call.argument(MethodArgs.BILLING_CHOICE_MODE);
182+
}
183+
startConnection(handle, result, billingChoiceMode);
151184
break;
152185
case MethodNames.END_CONNECTION:
153186
endConnection(result);
@@ -190,12 +223,61 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result
190223
case MethodNames.GET_BILLING_CONFIG:
191224
getBillingConfig(result);
192225
break;
226+
case MethodNames.IS_ALTERNATIVE_BILLING_ONLY_AVAILABLE:
227+
isAlternativeBillingOnlyAvailable(result);
228+
break;
229+
case MethodNames.CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS:
230+
createAlternativeBillingOnlyReportingDetails(result);
231+
break;
232+
case MethodNames.SHOW_ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG:
233+
showAlternativeBillingOnlyInformationDialog(result);
234+
break;
193235
default:
194236
result.notImplemented();
195237
}
196238
}
197239

240+
private void showAlternativeBillingOnlyInformationDialog(final MethodChannel.Result result) {
241+
if (billingClientError(result)) {
242+
return;
243+
}
244+
if (activity == null) {
245+
result.error(ACTIVITY_UNAVAILABLE, "Not attempting to show dialog", null);
246+
return;
247+
}
248+
billingClient.showAlternativeBillingOnlyInformationDialog(
249+
activity,
250+
billingResult -> {
251+
result.success(fromBillingResult(billingResult));
252+
});
253+
}
254+
255+
private void createAlternativeBillingOnlyReportingDetails(final MethodChannel.Result result) {
256+
if (billingClientError(result)) {
257+
return;
258+
}
259+
billingClient.createAlternativeBillingOnlyReportingDetailsAsync(
260+
((billingResult, alternativeBillingOnlyReportingDetails) -> {
261+
result.success(
262+
fromAlternativeBillingOnlyReportingDetails(
263+
billingResult, alternativeBillingOnlyReportingDetails));
264+
}));
265+
}
266+
267+
private void isAlternativeBillingOnlyAvailable(final MethodChannel.Result result) {
268+
if (billingClientError(result)) {
269+
return;
270+
}
271+
billingClient.isAlternativeBillingOnlyAvailableAsync(
272+
billingResult -> {
273+
result.success(fromBillingResult(billingResult));
274+
});
275+
}
276+
198277
private void getBillingConfig(final MethodChannel.Result result) {
278+
if (billingClientError(result)) {
279+
return;
280+
}
199281
billingClient.getBillingConfigAsync(
200282
GetBillingConfigParams.newBuilder().build(),
201283
(billingResult, billingConfig) -> {
@@ -313,7 +395,7 @@ private void launchBillingFlow(
313395

314396
if (activity == null) {
315397
result.error(
316-
"ACTIVITY_UNAVAILABLE",
398+
ACTIVITY_UNAVAILABLE,
317399
"Details for product "
318400
+ product
319401
+ " are not available. This method must be run with the app in foreground.",
@@ -422,9 +504,12 @@ private void getConnectionState(final MethodChannel.Result result) {
422504
result.success(serialized);
423505
}
424506

425-
private void startConnection(final int handle, final MethodChannel.Result result) {
507+
private void startConnection(
508+
final int handle, final MethodChannel.Result result, int billingChoiceMode) {
426509
if (billingClient == null) {
427-
billingClient = billingClientFactory.createBillingClient(applicationContext, methodChannel);
510+
billingClient =
511+
billingClientFactory.createBillingClient(
512+
applicationContext, methodChannel, billingChoiceMode);
428513
}
429514

430515
billingClient.startConnection(

packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import androidx.annotation.NonNull;
88
import androidx.annotation.Nullable;
99
import com.android.billingclient.api.AccountIdentifiers;
10+
import com.android.billingclient.api.AlternativeBillingOnlyReportingDetails;
1011
import com.android.billingclient.api.BillingConfig;
1112
import com.android.billingclient.api.BillingResult;
1213
import com.android.billingclient.api.ProductDetails;
@@ -240,6 +241,18 @@ static HashMap<String, Object> fromBillingConfig(
240241
return info;
241242
}
242243

244+
/**
245+
* Converter from {@link BillingResult} and {@link AlternativeBillingOnlyReportingDetails} to map.
246+
*/
247+
static HashMap<String, Object> fromAlternativeBillingOnlyReportingDetails(
248+
BillingResult result, AlternativeBillingOnlyReportingDetails details) {
249+
HashMap<String, Object> info = fromBillingResult(result);
250+
if (details != null) {
251+
info.put("externalTransactionToken", details.getExternalTransactionToken());
252+
}
253+
return info;
254+
}
255+
243256
/**
244257
* Gets the symbol of for the given currency code for the default {@link Locale.Category#DISPLAY
245258
* DISPLAY} locale. For example, for the US Dollar, the symbol is "$" if the default locale is the

0 commit comments

Comments
 (0)