Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit fe03e40

Browse files
author
Michael Klimushyn
authored
[IAP] Check if the payment processor is available (#1057)
The actual functionality here is extremely minimal. Most of this patch is establishing the structure of the rest of the plugin code to follow. 1. Add a generic IAP plugin interface with platform-specific implementations (`*_connection.dart`.) 2. Add platform specific wrappers (`*_wrappers.dart`). 3. Add dart end to end tests covering the sample app and unit tests covering dart and the platform code. These all need to be run manually currently. Fixes flutter/flutter#26321 flutter/flutter#25987
1 parent 43ee28c commit fe03e40

36 files changed

Lines changed: 1055 additions & 116 deletions

File tree

‎packages/in_app_purchase/README.md‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,17 @@ app](example/README.md) for an example on configuring both.
1919

2020
The API surface is stacked into 2 main layers.
2121

22-
1. [in_app_purchase.dart](lib/in_app_purchase.dart), the generic idiommatic
23-
Flutter API. This exposes the most basic IAP-related functionality. The goal
24-
is that Flutter apps should be able to use this API surface on its own for
25-
the vast majority of cases.
22+
1. [in_app_purchase_connection.dart](lib/src/in_app_purchase_connection.dart),
23+
the generic idiommatic Flutter API. This exposes the most basic IAP-related
24+
functionality. The goal is that Flutter apps should be able to use this API
25+
surface on its own for the vast majority of cases.
26+
[google_play_connection.dart](lib/src/google_play_connection.dart) and
27+
[app_store_connection.dart](lib/src/app_store_connection.dart) implement this
28+
for the specific platforms.
2629

2730
2. The dart wrappers around the platform specific IAP APIs and their platform
2831
specific implementations of the generic interface. See
29-
[google_play.dart](lib/google_play.dart) and
30-
[app_store.dart](lib/app_store.dart). These API surfaces should expose all
31-
the platform-specific behavior and allow for more fine-tuned control when
32-
needed.
32+
[store_kit_wrappers.dart](lib/src/store_kit_wrappers.dart) and
33+
[billing_client_wrappers.dart](lib/src/billing_client_wrappers.dart). These
34+
API surfaces should expose all the platform-specific behavior and allow for
35+
more fine-tuned control when needed.

‎packages/in_app_purchase/android/build.gradle‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,8 @@ android {
3535

3636
dependencies {
3737
implementation 'com.android.billingclient:billing:1.2'
38+
testImplementation 'junit:junit:4.12'
39+
testImplementation 'org.mockito:mockito-core:2.17.0'
40+
androidTestImplementation 'com.android.support.test:runner:1.0.2'
41+
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
3842
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#Mon Oct 29 10:30:44 PDT 2018
2+
distributionBase=GRADLE_USER_HOME
3+
distributionPath=wrapper/dists
4+
zipStoreBase=GRADLE_USER_HOME
5+
zipStorePath=wrapper/dists
6+
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

‎packages/in_app_purchase/android/src/main/java/io/flutter/inapppurchase/plugins/InAppPurchasePlugin.java‎

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package io.flutter.plugins.inapppurchase;
2+
3+
import android.content.Context;
4+
import android.support.annotation.Nullable;
5+
import android.support.annotation.VisibleForTesting;
6+
import com.android.billingclient.api.BillingClient;
7+
import com.android.billingclient.api.BillingClientStateListener;
8+
import com.android.billingclient.api.Purchase;
9+
import com.android.billingclient.api.PurchasesUpdatedListener;
10+
import io.flutter.plugin.common.MethodCall;
11+
import io.flutter.plugin.common.MethodChannel;
12+
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
13+
import io.flutter.plugin.common.MethodChannel.Result;
14+
import io.flutter.plugin.common.PluginRegistry.Registrar;
15+
import java.lang.Override;
16+
import java.util.HashMap;
17+
import java.util.List;
18+
import java.util.Map;
19+
20+
/** Wraps a {@link BillingClient} instance and responds to Dart calls for it. */
21+
public class InAppPurchasePlugin implements MethodCallHandler {
22+
private @Nullable BillingClient billingClient;
23+
private final Context context;
24+
private final MethodChannel channel;
25+
26+
/** Plugin registration. */
27+
public static void registerWith(Registrar registrar) {
28+
final MethodChannel channel =
29+
new MethodChannel(registrar.messenger(), "plugins.flutter.io/in_app_purchase");
30+
channel.setMethodCallHandler(new InAppPurchasePlugin(registrar.context(), channel));
31+
}
32+
33+
public InAppPurchasePlugin(Context context, MethodChannel channel) {
34+
this.context = context;
35+
this.channel = channel;
36+
}
37+
38+
@Override
39+
public void onMethodCall(MethodCall call, Result result) {
40+
switch (call.method) {
41+
case "BillingClient#isReady()":
42+
isReady(result);
43+
break;
44+
case "BillingClient#startConnection(BillingClientStateListener)":
45+
startConnection((int) call.argument("handle"), result);
46+
break;
47+
case "BillingClient#endConnection()":
48+
endConnection(result);
49+
break;
50+
default:
51+
result.notImplemented();
52+
}
53+
}
54+
55+
@VisibleForTesting
56+
/*package*/ InAppPurchasePlugin(BillingClient billingClient, MethodChannel channel) {
57+
this.billingClient = billingClient;
58+
this.channel = channel;
59+
this.context = null;
60+
}
61+
62+
private void startConnection(final int handle, final Result result) {
63+
if (billingClient == null) {
64+
billingClient = buildBillingClient(context);
65+
}
66+
67+
billingClient.startConnection(
68+
new BillingClientStateListener() {
69+
@Override
70+
public void onBillingSetupFinished(int responseCode) {
71+
// Consider the fact that we've finished a success, leave it to the Dart side to validate the responseCode.
72+
result.success(responseCode);
73+
}
74+
75+
@Override
76+
public void onBillingServiceDisconnected() {
77+
final Map<String, Object> arguments = new HashMap<>();
78+
arguments.put("handle", handle);
79+
channel.invokeMethod(
80+
"BillingClientStateListener#onBillingServiceDisconnected()", arguments);
81+
}
82+
});
83+
}
84+
85+
private void endConnection(final Result result) {
86+
billingClient.endConnection();
87+
billingClient = null;
88+
result.success(null);
89+
}
90+
91+
private void isReady(Result result) {
92+
result.success(billingClient.isReady());
93+
}
94+
95+
private static BillingClient buildBillingClient(Context context) {
96+
return BillingClient.newBuilder(context)
97+
.setListener(
98+
new PurchasesUpdatedListener() {
99+
@Override
100+
public void onPurchasesUpdated(
101+
int responseCode, @Nullable List<Purchase> purchases) {}
102+
})
103+
.build();
104+
}
105+
}

‎packages/in_app_purchase/example/android/app/build.gradle‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ flutter {
108108
}
109109

110110
dependencies {
111+
implementation 'com.android.billingclient:billing:1.2'
111112
testImplementation 'junit:junit:4.12'
113+
testImplementation 'org.mockito:mockito-core:2.17.0'
112114
androidTestImplementation 'com.android.support.test:runner:1.0.2'
113115
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
114116
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package io.flutter.plugins.inapppurchase;
2+
3+
import static org.mockito.ArgumentMatchers.any;
4+
import static org.mockito.Mockito.doNothing;
5+
import static org.mockito.Mockito.mock;
6+
import static org.mockito.Mockito.never;
7+
import static org.mockito.Mockito.times;
8+
import static org.mockito.Mockito.verify;
9+
import static org.mockito.Mockito.when;
10+
11+
import com.android.billingclient.api.BillingClient;
12+
import com.android.billingclient.api.BillingClientStateListener;
13+
import io.flutter.plugin.common.MethodCall;
14+
import io.flutter.plugin.common.MethodChannel;
15+
import io.flutter.plugin.common.MethodChannel.Result;
16+
import java.util.HashMap;
17+
import java.util.Map;
18+
import org.junit.Before;
19+
import org.junit.Test;
20+
import org.mockito.ArgumentCaptor;
21+
import org.mockito.Mock;
22+
import org.mockito.MockitoAnnotations;
23+
import org.mockito.Spy;
24+
25+
public class InAppPurchasePluginTest {
26+
InAppPurchasePlugin plugin;
27+
@Mock BillingClient mockBillingClient;
28+
@Mock MethodChannel mockMethodChannel;
29+
@Spy Result result;
30+
31+
@Before
32+
public void setUp() {
33+
MockitoAnnotations.initMocks(this);
34+
plugin = new InAppPurchasePlugin(mockBillingClient, mockMethodChannel);
35+
}
36+
37+
@Test
38+
public void invalidMethod() {
39+
MethodCall call = new MethodCall("invalid", null);
40+
plugin.onMethodCall(call, result);
41+
verify(result, times(1)).notImplemented();
42+
}
43+
44+
@Test
45+
public void isReady_true() {
46+
MethodCall call = new MethodCall("BillingClient#isReady()", null);
47+
when(mockBillingClient.isReady()).thenReturn(true);
48+
plugin.onMethodCall(call, result);
49+
verify(result).success(true);
50+
}
51+
52+
@Test
53+
public void isReady_false() {
54+
MethodCall call = new MethodCall("BillingClient#isReady()", null);
55+
when(mockBillingClient.isReady()).thenReturn(false);
56+
plugin.onMethodCall(call, result);
57+
verify(result).success(false);
58+
}
59+
60+
@Test
61+
public void startConnection() {
62+
Map<String, Integer> arguments = new HashMap<>();
63+
arguments.put("handle", 1);
64+
MethodCall call =
65+
new MethodCall("BillingClient#startConnection(BillingClientStateListener)", arguments);
66+
ArgumentCaptor<BillingClientStateListener> captor =
67+
ArgumentCaptor.forClass(BillingClientStateListener.class);
68+
doNothing().when(mockBillingClient).startConnection(captor.capture());
69+
70+
plugin.onMethodCall(call, result);
71+
verify(result, never()).success(any());
72+
captor.getValue().onBillingSetupFinished(100);
73+
74+
verify(result, times(1)).success(100);
75+
}
76+
77+
@Test
78+
public void endConnection() {
79+
// Set up a connected BillingClient instance
80+
final int disconnectCallbackHandle = 22;
81+
Map<String, Integer> arguments = new HashMap<>();
82+
arguments.put("handle", disconnectCallbackHandle);
83+
MethodCall connectCall =
84+
new MethodCall("BillingClient#startConnection(BillingClientStateListener)", arguments);
85+
ArgumentCaptor<BillingClientStateListener> captor =
86+
ArgumentCaptor.forClass(BillingClientStateListener.class);
87+
doNothing().when(mockBillingClient).startConnection(captor.capture());
88+
plugin.onMethodCall(connectCall, mock(Result.class));
89+
final BillingClientStateListener stateListener = captor.getValue();
90+
91+
// Disconnect the connected client
92+
MethodCall disconnectCall = new MethodCall("BillingClient#endConnection()", null);
93+
plugin.onMethodCall(disconnectCall, result);
94+
95+
// Verify that the client is disconnected and that the OnDisconnect callback has been triggered
96+
verify(result, times(1)).success(any());
97+
verify(mockBillingClient, times(1)).endConnection();
98+
stateListener.onBillingServiceDisconnected();
99+
Map<String, Integer> expectedInvocation = new HashMap<>();
100+
expectedInvocation.put("handle", disconnectCallbackHandle);
101+
verify(mockMethodChannel, times(1))
102+
.invokeMethod(
103+
"BillingClientStateListener#onBillingServiceDisconnected()", expectedInvocation);
104+
}
105+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mock-maker-inline

0 commit comments

Comments
 (0)