Skip to content

Commit 2c15d86

Browse files
authored
[in_app_purchase_android] Cleanup, create listener where it is used (#6489)
Fixes [144851](flutter/flutter#144851)
1 parent b855307 commit 2c15d86

File tree

8 files changed

+146
-97
lines changed

8 files changed

+146
-97
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.3+1
2+
3+
* Moves alternative billing listener creation to BillingClientFactoryImpl.
4+
15
## 0.3.3
26

37
* Converts data objects in internal platform communication to Pigeon.

packages/in_app_purchase/in_app_purchase_android/android/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ dependencies {
6666
testImplementation 'junit:junit:4.13.2'
6767
testImplementation 'org.json:json:20240303'
6868
testImplementation 'org.mockito:mockito-core:5.4.0'
69+
testImplementation 'androidx.test:core:1.5.0'
70+
testImplementation 'org.robolectric:robolectric:4.10.3'
6971
androidTestImplementation 'androidx.test:runner:1.5.2'
7072
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
7173
}

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66

77
import android.content.Context;
88
import androidx.annotation.NonNull;
9-
import androidx.annotation.Nullable;
109
import com.android.billingclient.api.BillingClient;
11-
import com.android.billingclient.api.UserChoiceBillingListener;
1210
import io.flutter.plugins.inapppurchase.Messages.PlatformBillingChoiceMode;
1311

1412
/** Responsible for creating a {@link BillingClient} object. */
@@ -26,6 +24,5 @@ interface BillingClientFactory {
2624
BillingClient createBillingClient(
2725
@NonNull Context context,
2826
@NonNull Messages.InAppPurchaseCallbackApi callbackApi,
29-
PlatformBillingChoiceMode billingChoiceMode,
30-
@Nullable UserChoiceBillingListener userChoiceBillingListener);
27+
PlatformBillingChoiceMode billingChoiceMode);
3128
}

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

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
package io.flutter.plugins.inapppurchase;
66

7+
import static io.flutter.plugins.inapppurchase.Translator.fromUserChoiceDetails;
8+
79
import android.content.Context;
810
import androidx.annotation.NonNull;
9-
import androidx.annotation.Nullable;
11+
import androidx.annotation.VisibleForTesting;
1012
import com.android.billingclient.api.BillingClient;
1113
import com.android.billingclient.api.UserChoiceBillingListener;
1214
import io.flutter.Log;
@@ -19,23 +21,15 @@ final class BillingClientFactoryImpl implements BillingClientFactory {
1921
public BillingClient createBillingClient(
2022
@NonNull Context context,
2123
@NonNull Messages.InAppPurchaseCallbackApi callbackApi,
22-
PlatformBillingChoiceMode billingChoiceMode,
23-
@Nullable UserChoiceBillingListener userChoiceBillingListener) {
24+
PlatformBillingChoiceMode billingChoiceMode) {
2425
BillingClient.Builder builder = BillingClient.newBuilder(context).enablePendingPurchases();
2526
switch (billingChoiceMode) {
2627
case ALTERNATIVE_BILLING_ONLY:
2728
// https://developer.android.com/google/play/billing/alternative/alternative-billing-without-user-choice-in-app
2829
builder.enableAlternativeBillingOnly();
2930
break;
3031
case USER_CHOICE_BILLING:
31-
if (userChoiceBillingListener != null) {
32-
// https://developer.android.com/google/play/billing/alternative/alternative-billing-with-user-choice-in-app
33-
builder.enableUserChoiceBilling(userChoiceBillingListener);
34-
} else {
35-
Log.e(
36-
"BillingClientFactoryImpl",
37-
"userChoiceBillingListener null when USER_CHOICE_BILLING set. Defaulting to PLAY_BILLING_ONLY");
38-
}
32+
builder.enableUserChoiceBilling(createUserChoiceBillingListener(callbackApi));
3933
break;
4034
case PLAY_BILLING_ONLY:
4135
// Do nothing.
@@ -48,4 +42,22 @@ public BillingClient createBillingClient(
4842
}
4943
return builder.setListener(new PluginPurchaseListener(callbackApi)).build();
5044
}
45+
46+
@VisibleForTesting
47+
/* package */ UserChoiceBillingListener createUserChoiceBillingListener(
48+
@NonNull Messages.InAppPurchaseCallbackApi callbackApi) {
49+
return userChoiceDetails ->
50+
callbackApi.userSelectedalternativeBilling(
51+
fromUserChoiceDetails(userChoiceDetails),
52+
new Messages.VoidResult() {
53+
@Override
54+
public void success() {}
55+
56+
@Override
57+
public void error(@NonNull Throwable error) {
58+
io.flutter.Log.e(
59+
"IN_APP_PURCHASE", "userSelectedalternativeBilling handler error: " + error);
60+
}
61+
});
62+
}
5163
}

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

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList;
1111
import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList;
1212
import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList;
13-
import static io.flutter.plugins.inapppurchase.Translator.fromUserChoiceDetails;
1413
import static io.flutter.plugins.inapppurchase.Translator.toProductList;
1514
import static io.flutter.plugins.inapppurchase.Translator.toProductTypeString;
1615

@@ -34,7 +33,6 @@
3433
import com.android.billingclient.api.QueryProductDetailsParams;
3534
import com.android.billingclient.api.QueryPurchaseHistoryParams;
3635
import com.android.billingclient.api.QueryPurchasesParams;
37-
import com.android.billingclient.api.UserChoiceBillingListener;
3836
import io.flutter.plugins.inapppurchase.Messages.FlutterError;
3937
import io.flutter.plugins.inapppurchase.Messages.InAppPurchaseApi;
4038
import io.flutter.plugins.inapppurchase.Messages.InAppPurchaseCallbackApi;
@@ -437,10 +435,8 @@ public void startConnection(
437435
@NonNull PlatformBillingChoiceMode billingMode,
438436
@NonNull Result<PlatformBillingResult> result) {
439437
if (billingClient == null) {
440-
UserChoiceBillingListener listener = getUserChoiceBillingListener(billingMode);
441438
billingClient =
442-
billingClientFactory.createBillingClient(
443-
applicationContext, callbackApi, billingMode, listener);
439+
billingClientFactory.createBillingClient(applicationContext, callbackApi, billingMode);
444440
}
445441

446442
try {
@@ -482,30 +478,6 @@ public void error(@NonNull Throwable error) {
482478
}
483479
}
484480

485-
@Nullable
486-
private UserChoiceBillingListener getUserChoiceBillingListener(
487-
PlatformBillingChoiceMode billingChoiceMode) {
488-
UserChoiceBillingListener listener = null;
489-
if (billingChoiceMode == PlatformBillingChoiceMode.USER_CHOICE_BILLING) {
490-
listener =
491-
userChoiceDetails ->
492-
callbackApi.userSelectedalternativeBilling(
493-
fromUserChoiceDetails(userChoiceDetails),
494-
new Messages.VoidResult() {
495-
@Override
496-
public void success() {}
497-
498-
@Override
499-
public void error(@NonNull Throwable error) {
500-
io.flutter.Log.e(
501-
"IN_APP_PURCHASE",
502-
"userSelectedalternativeBilling handler error: " + error);
503-
}
504-
});
505-
}
506-
return listener;
507-
}
508-
509481
@Override
510482
public void acknowledgePurchase(
511483
@NonNull String purchaseToken, @NonNull Result<PlatformBillingResult> result) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.inapppurchase;
6+
7+
import static org.junit.Assert.assertEquals;
8+
import static org.junit.Assert.assertNotNull;
9+
import static org.junit.Assert.assertTrue;
10+
import static org.mockito.ArgumentMatchers.any;
11+
import static org.mockito.Mockito.doReturn;
12+
import static org.mockito.Mockito.mock;
13+
import static org.mockito.Mockito.spy;
14+
import static org.mockito.Mockito.times;
15+
import static org.mockito.Mockito.verify;
16+
import static org.mockito.Mockito.when;
17+
18+
import android.content.Context;
19+
import androidx.test.core.app.ApplicationProvider;
20+
import com.android.billingclient.api.BillingClient;
21+
import com.android.billingclient.api.UserChoiceBillingListener;
22+
import com.android.billingclient.api.UserChoiceDetails;
23+
import io.flutter.plugins.inapppurchase.Messages.InAppPurchaseCallbackApi;
24+
import io.flutter.plugins.inapppurchase.Messages.PlatformBillingChoiceMode;
25+
import io.flutter.plugins.inapppurchase.Messages.PlatformUserChoiceDetails;
26+
import java.util.Collections;
27+
import org.junit.After;
28+
import org.junit.Before;
29+
import org.junit.Test;
30+
import org.junit.runner.RunWith;
31+
import org.mockito.ArgumentCaptor;
32+
import org.mockito.Mock;
33+
import org.mockito.MockitoAnnotations;
34+
import org.robolectric.RobolectricTestRunner;
35+
36+
@RunWith(RobolectricTestRunner.class)
37+
public class BillingClientFactoryImplTest {
38+
39+
private AutoCloseable openMocks;
40+
BillingClientFactoryImpl factory;
41+
@Mock InAppPurchaseCallbackApi mockCallbackApi;
42+
Context context;
43+
44+
@Before
45+
public void setUp() {
46+
openMocks = MockitoAnnotations.openMocks(this);
47+
// Context must be a "real/robolectric" context since the implementation of billing client
48+
// calls methods on context.
49+
context = ApplicationProvider.getApplicationContext();
50+
factory = spy(new BillingClientFactoryImpl());
51+
}
52+
53+
@Test
54+
public void playBillingOnly() {
55+
// No logic to verify just ensure creation works.
56+
BillingClient client =
57+
factory.createBillingClient(
58+
context, mockCallbackApi, PlatformBillingChoiceMode.PLAY_BILLING_ONLY);
59+
assertNotNull(client);
60+
}
61+
62+
@Test
63+
public void alternativeBillingOnly() {
64+
// No logic to verify just ensure creation works.
65+
BillingClient client =
66+
factory.createBillingClient(
67+
context, mockCallbackApi, PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY);
68+
assertNotNull(client);
69+
}
70+
71+
@Test
72+
public void userChoiceBilling() {
73+
final UserChoiceBillingListener listener =
74+
factory.createUserChoiceBillingListener(mockCallbackApi);
75+
doReturn(listener)
76+
.when(factory)
77+
.createUserChoiceBillingListener(any(InAppPurchaseCallbackApi.class));
78+
79+
final BillingClient billingClient =
80+
factory.createBillingClient(
81+
context, mockCallbackApi, PlatformBillingChoiceMode.USER_CHOICE_BILLING);
82+
83+
UserChoiceDetails details = mock(UserChoiceDetails.class);
84+
final String externalTransactionToken = "someLongTokenId1234";
85+
final String originalTransactionId = "originalTransactionId123456";
86+
when(details.getExternalTransactionToken()).thenReturn(externalTransactionToken);
87+
when(details.getOriginalExternalTransactionId()).thenReturn(originalTransactionId);
88+
when(details.getProducts()).thenReturn(Collections.emptyList());
89+
listener.userSelectedAlternativeBilling(details);
90+
91+
ArgumentCaptor<PlatformUserChoiceDetails> callbackCaptor =
92+
ArgumentCaptor.forClass(PlatformUserChoiceDetails.class);
93+
verify(mockCallbackApi, times(1))
94+
.userSelectedalternativeBilling(callbackCaptor.capture(), any());
95+
assertEquals(callbackCaptor.getValue().getExternalTransactionToken(), externalTransactionToken);
96+
assertEquals(
97+
callbackCaptor.getValue().getOriginalExternalTransactionId(), originalTransactionId);
98+
assertTrue(callbackCaptor.getValue().getProducts().isEmpty());
99+
}
100+
101+
@After
102+
public void tearDown() throws Exception {
103+
openMocks.close();
104+
}
105+
}

0 commit comments

Comments
 (0)