Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -4,6 +4,8 @@

package io.flutter.plugins.inapppurchase;

import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList;
import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesResult;
import static io.flutter.plugins.inapppurchase.Translator.fromSkuDetailsList;

import android.app.Activity;
Expand All @@ -14,6 +16,7 @@
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchaseHistoryResponseListener;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
Expand Down Expand Up @@ -46,6 +49,11 @@ static final class MethodNames {
"BillingClient#querySkuDetailsAsync(SkuDetailsParams, SkuDetailsResponseListener)";
static final String LAUNCH_BILLING_FLOW =
"BillingClient#launchBillingFlow(Activity, BillingFlowParams)";
static final String ON_PURCHASES_UPDATED =
"PurchasesUpdatedListener#onPurchasesUpdated(int, List<Purchase>)";
static final String QUERY_PURCHASES = "queryPurchases(String)";
static final String QUERY_PURCHASE_HISTORY_ASYNC =
"queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)";

private MethodNames() {};
}
Expand Down Expand Up @@ -86,6 +94,12 @@ public void onMethodCall(MethodCall call, Result result) {
launchBillingFlow(
(String) call.argument("sku"), (String) call.argument("accountId"), result);
break;
case MethodNames.QUERY_PURCHASES:
queryPurchases((String) call.argument("skuType"), result);
break;
case MethodNames.QUERY_PURCHASE_HISTORY_ASYNC:
queryPurchaseHistoryAsync((String) call.argument("skuType"), result);
break;
default:
result.notImplemented();
}
Expand All @@ -101,7 +115,7 @@ public void onMethodCall(MethodCall call, Result result) {

private void startConnection(final int handle, final Result result) {
if (billingClient == null) {
billingClient = buildBillingClient(context);
billingClient = buildBillingClient(context, channel);
}

billingClient.startConnection(
Expand Down Expand Up @@ -181,7 +195,38 @@ private void launchBillingFlow(String sku, @Nullable String accountId, Result re
result.success(billingClient.launchBillingFlow(activity, paramsBuilder.build()));
}

private void updateCachedSkus(List<SkuDetails> skuDetailsList) {
private void queryPurchases(String skuType, Result result) {
if (billingClientError(result)) {
return;
}

// Like in our connect call, consider the billing client responding a "success" here regardless of status code.
result.success(fromPurchasesResult(billingClient.queryPurchases(skuType)));
}

private void queryPurchaseHistoryAsync(String skuType, final Result result) {
if (billingClientError(result)) {
return;
}

billingClient.queryPurchaseHistoryAsync(
skuType,
new PurchaseHistoryResponseListener() {
@Override
public void onPurchaseHistoryResponse(int responseCode, List<Purchase> purchasesList) {
final Map<String, Object> serialized = new HashMap<>();
serialized.put("responseCode", responseCode);
serialized.put("purchasesList", fromPurchasesList(purchasesList));
result.success(serialized);
}
});
}

private void updateCachedSkus(@Nullable List<SkuDetails> skuDetailsList) {
if (skuDetailsList == null) {
return;
}

for (SkuDetails skuDetails : skuDetailsList) {
cachedSkus.put(skuDetails.getSku(), skuDetails);
}
Expand All @@ -196,14 +241,26 @@ private boolean billingClientError(Result result) {
return true;
}

private static BillingClient buildBillingClient(Context context) {
private static BillingClient buildBillingClient(Context context, MethodChannel channel) {
return BillingClient.newBuilder(context)
.setListener(
new PurchasesUpdatedListener() {
@Override
public void onPurchasesUpdated(
int responseCode, @Nullable List<Purchase> purchases) {}
})
.setListener(new PluginPurchaseListener(channel))
.build();
}

@VisibleForTesting
/*package*/ static class PluginPurchaseListener implements PurchasesUpdatedListener {
private final MethodChannel channel;

PluginPurchaseListener(MethodChannel channel) {
this.channel = channel;
}

@Override
public void onPurchasesUpdated(int responseCode, @Nullable List<Purchase> purchases) {
final Map<String, Object> callbackArgs = new HashMap<>();
callbackArgs.put("responseCode", responseCode);
callbackArgs.put("purchases", fromPurchasesList(purchases));
channel.invokeMethod(MethodNames.ON_PURCHASES_UPDATED, callbackArgs);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package io.flutter.plugins.inapppurchase;

import androidx.annotation.Nullable;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.Purchase.PurchasesResult;
import com.android.billingclient.api.SkuDetails;
import java.util.ArrayList;
import java.util.Collections;
Expand Down Expand Up @@ -44,4 +46,35 @@ static List<HashMap<String, Object>> fromSkuDetailsList(
}
return output;
}

static HashMap<String, Object> fromPurchase(Purchase purchase) {
HashMap<String, Object> info = new HashMap<>();
info.put("orderId", purchase.getOrderId());
info.put("packageName", purchase.getPackageName());
info.put("purchaseTime", purchase.getPurchaseTime());
info.put("purchaseToken", purchase.getPurchaseToken());
info.put("signature", purchase.getSignature());
info.put("sku", purchase.getSku());
info.put("isAutoRenewing", purchase.isAutoRenewing());
return info;
}

static List<HashMap<String, Object>> fromPurchasesList(@Nullable List<Purchase> purchases) {
if (purchases == null) {
return Collections.emptyList();
}

List<HashMap<String, Object>> serialized = new ArrayList<>();
for (Purchase purchase : purchases) {
serialized.add(fromPurchase(purchase));
}
return serialized;
}

static HashMap<String, Object> fromPurchasesResult(PurchasesResult purchasesResult) {
HashMap<String, Object> info = new HashMap<>();
info.put("responseCode", purchasesResult.getResponseCode());
info.put("purchasesList", fromPurchasesList(purchasesResult.getPurchasesList()));
return info;
}
}
16 changes: 4 additions & 12 deletions packages/in_app_purchase/example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ project.ext {
KEYSTORE_STORE_PASSWORD = keystoreProperties['storePassword']
KEYSTORE_KEY_ALIAS = keystoreProperties['keyAlias']
KEYSTORE_KEY_PASSWORD = keystoreProperties['keyPassword']
VERSION_CODE = 1
VERSION_NAME = "0.0.1"
}

if (project.APP_ID == "io.flutter.plugins.inapppurchaseexample.DEFAULT_DO_NOT_USE") {
Expand All @@ -46,16 +48,6 @@ if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}

def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}

def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}

apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

Expand All @@ -79,8 +71,8 @@ android {
applicationId project.APP_ID
minSdkVersion 16
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
versionCode project.VERSION_CODE
versionName project.VERSION_NAME
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@
import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.IS_READY;
import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.LAUNCH_BILLING_FLOW;
import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ON_DISCONNECT;
import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ON_PURCHASES_UPDATED;
import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_PURCHASES;
import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC;
import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_SKU_DETAILS;
import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.START_CONNECTION;
import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList;
import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesResult;
import static io.flutter.plugins.inapppurchase.Translator.fromSkuDetailsList;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
Expand All @@ -14,6 +19,7 @@
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
Expand All @@ -27,12 +33,17 @@
import com.android.billingclient.api.BillingClient.SkuType;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.Purchase.PurchasesResult;
import com.android.billingclient.api.PurchaseHistoryResponseListener;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugins.inapppurchase.InAppPurchasePlugin.PluginPurchaseListener;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -279,6 +290,93 @@ public void launchBillingFlow_skuNotFound() {
verify(result, never()).success(any());
}

@Test
public void queryPurchases() {
establishConnectedBillingClient(null, null);
PurchasesResult purchasesResult = mock(PurchasesResult.class);
when(purchasesResult.getResponseCode()).thenReturn(BillingResponse.OK);
Purchase purchase = buildPurchase("foo");
when(purchasesResult.getPurchasesList()).thenReturn(asList(purchase));
when(mockBillingClient.queryPurchases(SkuType.INAPP)).thenReturn(purchasesResult);

HashMap<String, Object> arguments = new HashMap<>();
arguments.put("skuType", SkuType.INAPP);
plugin.onMethodCall(new MethodCall(QUERY_PURCHASES, arguments), result);

// Verify we pass the response to result
ArgumentCaptor<HashMap<String, Object>> resultCaptor = ArgumentCaptor.forClass(HashMap.class);
verify(result, never()).error(any(), any(), any());
verify(result, times(1)).success(resultCaptor.capture());
assertEquals(fromPurchasesResult(purchasesResult), resultCaptor.getValue());
}

@Test
public void queryPurchases_clientDisconnected() {
// Prepare the launch call after disconnecting the client
plugin.onMethodCall(new MethodCall(END_CONNECTION, null), mock(Result.class));

HashMap<String, Object> arguments = new HashMap<>();
arguments.put("skuType", SkuType.INAPP);
plugin.onMethodCall(new MethodCall(QUERY_PURCHASES, arguments), result);

// Assert that we sent an error back.
verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any());
verify(result, never()).success(any());
}

@Test
public void queryPurchaseHistoryAsync() {
// Set up an established billing client and all our mocked responses
establishConnectedBillingClient(null, null);
ArgumentCaptor<HashMap<String, Object>> resultCaptor = ArgumentCaptor.forClass(HashMap.class);
int responseCode = BillingResponse.OK;
List<Purchase> purchasesList = asList(buildPurchase("foo"));
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("skuType", SkuType.INAPP);
ArgumentCaptor<PurchaseHistoryResponseListener> listenerCaptor =
ArgumentCaptor.forClass(PurchaseHistoryResponseListener.class);

plugin.onMethodCall(new MethodCall(QUERY_PURCHASE_HISTORY_ASYNC, arguments), result);

// Verify we pass the data to result
verify(mockBillingClient)
.queryPurchaseHistoryAsync(eq(SkuType.INAPP), listenerCaptor.capture());
listenerCaptor.getValue().onPurchaseHistoryResponse(responseCode, purchasesList);
verify(result).success(resultCaptor.capture());
HashMap<String, Object> resultData = resultCaptor.getValue();
assertEquals(responseCode, resultData.get("responseCode"));
assertEquals(fromPurchasesList(purchasesList), resultData.get("purchasesList"));
}

@Test
public void queryPurchaseHistoryAsync_clientDisconnected() {
// Prepare the launch call after disconnecting the client
plugin.onMethodCall(new MethodCall(END_CONNECTION, null), mock(Result.class));

HashMap<String, Object> arguments = new HashMap<>();
arguments.put("skuType", SkuType.INAPP);
plugin.onMethodCall(new MethodCall(QUERY_PURCHASE_HISTORY_ASYNC, arguments), result);

// Assert that we sent an error back.
verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any());
verify(result, never()).success(any());
}

@Test
public void onPurchasesUpdatedListener() {
PluginPurchaseListener listener = new PluginPurchaseListener(mockMethodChannel);

int responseCode = BillingResponse.OK;
List<Purchase> purchasesList = asList(buildPurchase("foo"));
ArgumentCaptor<HashMap<String, Object>> resultCaptor = ArgumentCaptor.forClass(HashMap.class);
doNothing().when(mockMethodChannel).invokeMethod(eq(ON_PURCHASES_UPDATED), resultCaptor.capture());
listener.onPurchasesUpdated(responseCode, purchasesList);

HashMap<String, Object> resultData = resultCaptor.getValue();
assertEquals(responseCode, resultData.get("responseCode"));
assertEquals(fromPurchasesList(purchasesList), resultData.get("purchases"));
}

private void establishConnectedBillingClient(
@Nullable Map<String, Integer> arguments, @Nullable Result result) {
if (arguments == null) {
Expand Down Expand Up @@ -319,4 +417,10 @@ private SkuDetails buildSkuDetails(String id) {
when(details.getSku()).thenReturn(id);
return details;
}

private Purchase buildPurchase(String orderId) {
Purchase purchase = mock(Purchase.class);
when(purchase.getOrderId()).thenReturn(orderId);
return purchase;
}
}
Loading