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

[in_app_purchase] Migrate to Billing to 5.0.0 #5405

Merged
merged 22 commits into from
Jul 8, 2022
Merged
Show file tree
Hide file tree
Changes from 17 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 @@ -52,11 +52,11 @@ android {
}

dependencies {
implementation 'androidx.annotation:annotation:1.0.0'
implementation 'com.android.billingclient:billing:3.0.2'
implementation 'androidx.annotation:annotation:1.3.0'
implementation 'com.android.billingclient:billing:5.0.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20180813'
testImplementation 'org.mockito:mockito-core:3.6.0'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
testImplementation 'org.json:json:20220320'
testImplementation 'org.mockito:mockito-core:4.5.1'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ static final class MethodNames {
static final String ON_PURCHASES_UPDATED =
"PurchasesUpdatedListener#onPurchasesUpdated(int, List<Purchase>)";
static final String QUERY_PURCHASES = "BillingClient#queryPurchases(String)";
static final String QUERY_PURCHASES_ASYNC = "BillingClient#queryPurchasesAsync(String)";
static final String QUERY_PURCHASE_HISTORY_ASYNC =
"BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)";
static final String CONSUME_PURCHASE_ASYNC =
Expand All @@ -48,6 +49,7 @@ static final class MethodNames {
static final String IS_FEATURE_SUPPORTED = "BillingClient#isFeatureSupported(String)";
static final String LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW =
"BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)";
static final String GET_CONNECTION_STATE = "BillingClient#getConnectionState()";

private MethodNames() {};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
package io.flutter.plugins.inapppurchase;

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

import android.app.Activity;
Expand All @@ -25,11 +25,15 @@
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.PriceChangeFlowParams;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchaseHistoryRecord;
import com.android.billingclient.api.PurchaseHistoryResponseListener;
import com.android.billingclient.api.PurchasesResponseListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;
import com.android.billingclient.api.QueryPurchaseHistoryParams;
import com.android.billingclient.api.QueryPurchasesParams;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import java.util.HashMap;
Expand Down Expand Up @@ -131,8 +135,11 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
: ProrationMode.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY,
result);
break;
case InAppPurchasePlugin.MethodNames.QUERY_PURCHASES:
queryPurchases((String) call.argument("skuType"), result);
case InAppPurchasePlugin.MethodNames.QUERY_PURCHASES: // Legacy method name.
queryPurchasesAsync((String) call.argument("skuType"), result);
break;
case InAppPurchasePlugin.MethodNames.QUERY_PURCHASES_ASYNC:
queryPurchasesAsync((String) call.argument("skuType"), result);
break;
case InAppPurchasePlugin.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC:
queryPurchaseHistoryAsync((String) call.argument("skuType"), result);
Expand All @@ -149,6 +156,9 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
case InAppPurchasePlugin.MethodNames.LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW:
launchPriceChangeConfirmationFlow((String) call.argument("sku"), result);
break;
case InAppPurchasePlugin.MethodNames.GET_CONNECTION_STATE:
getConnectionState(result);
break;
default:
result.notImplemented();
}
Expand All @@ -174,6 +184,7 @@ private void isReady(MethodChannel.Result result) {
result.success(billingClient.isReady());
}

// TODO(garyq): Migrate to new subscriptions API: https://developer.android.com/google/play/billing/migrate-gpblv5
private void querySkuDetailsAsync(
final String skuType, final List<String> skusList, final MethodChannel.Result result) {
if (billingClientError(result)) {
Expand Down Expand Up @@ -256,11 +267,14 @@ private void launchBillingFlow(
paramsBuilder.setObfuscatedProfileId(obfuscatedProfileId);
}
if (oldSku != null && !oldSku.isEmpty()) {
paramsBuilder.setOldSku(oldSku, purchaseToken);
BillingFlowParams.SubscriptionUpdateParams.Builder subscriptionUpdateParamsBuilder =
BillingFlowParams.SubscriptionUpdateParams.newBuilder();
subscriptionUpdateParamsBuilder.setOldSkuPurchaseToken(purchaseToken);
// The proration mode value has to match one of the following declared in
// https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode
subscriptionUpdateParamsBuilder.setReplaceSkusProrationMode(prorationMode);
paramsBuilder.setSubscriptionUpdateParams(subscriptionUpdateParamsBuilder.build());
}
// The proration mode value has to match one of the following declared in
// https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode
paramsBuilder.setReplaceSkusProrationMode(prorationMode);
result.success(
Translator.fromBillingResult(
billingClient.launchBillingFlow(activity, paramsBuilder.build())));
Expand All @@ -286,14 +300,31 @@ public void onConsumeResponse(BillingResult billingResult, String outToken) {
billingClient.consumeAsync(params, listener);
}

private void queryPurchases(String skuType, MethodChannel.Result result) {
private void queryPurchasesAsync(String skuType, MethodChannel.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)));
QueryPurchasesParams.Builder paramsBuilder = QueryPurchasesParams.newBuilder();
paramsBuilder.setProductType(skuType);
billingClient.queryPurchasesAsync(
paramsBuilder.build(),
new PurchasesResponseListener() {
@Override
public void onQueryPurchasesResponse(
BillingResult billingResult, List<Purchase> purchasesList) {
final Map<String, Object> serialized = new HashMap<>();
// The response code is no longer passed, as part of billing 4.0, so we pass OK here
// as success is implied by calling this callback.
serialized.put("responseCode", BillingClient.BillingResponseCode.OK);
serialized.put("billingResult", Translator.fromBillingResult(billingResult));
serialized.put(
"purchaseList", fromPurchasesList(purchasesList));
result.success(serialized);
}
});
}

private void queryPurchaseHistoryAsync(String skuType, final MethodChannel.Result result) {
Expand All @@ -302,7 +333,7 @@ private void queryPurchaseHistoryAsync(String skuType, final MethodChannel.Resul
}

billingClient.queryPurchaseHistoryAsync(
skuType,
QueryPurchaseHistoryParams.newBuilder().setProductType(skuType).build(),
new PurchaseHistoryResponseListener() {
@Override
public void onPurchaseHistoryResponse(
Expand All @@ -316,6 +347,15 @@ public void onPurchaseHistoryResponse(
});
}

private void getConnectionState(final MethodChannel.Result result) {
if (billingClientError(result)) {
return;
}
final Map<String, Object> serialized = new HashMap<>();
serialized.put("connectionState", billingClient.getConnectionState());
result.success(serialized);
}

private void startConnection(final int handle, final MethodChannel.Result result) {
if (billingClient == null) {
billingClient = billingClientFactory.createBillingClient(applicationContext, methodChannel);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import com.android.billingclient.api.AccountIdentifiers;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.Purchase.PurchasesResult;
import com.android.billingclient.api.PurchaseHistoryRecord;
import com.android.billingclient.api.SkuDetails;
import java.util.ArrayList;
Expand Down Expand Up @@ -56,17 +55,19 @@ static List<HashMap<String, Object>> fromSkuDetailsList(

static HashMap<String, Object> fromPurchase(Purchase purchase) {
HashMap<String, Object> info = new HashMap<>();
List<String> skus = purchase.getSkus();
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("skus", skus);
info.put("isAutoRenewing", purchase.isAutoRenewing());
info.put("originalJson", purchase.getOriginalJson());
info.put("developerPayload", purchase.getDeveloperPayload());
info.put("isAcknowledged", purchase.isAcknowledged());
info.put("purchaseState", purchase.getPurchaseState());
info.put("quantity", purchase.getQuantity());
AccountIdentifiers accountIdentifiers = purchase.getAccountIdentifiers();
if (accountIdentifiers != null) {
info.put("obfuscatedAccountId", accountIdentifiers.getObfuscatedAccountId());
Expand All @@ -78,12 +79,14 @@ static HashMap<String, Object> fromPurchase(Purchase purchase) {
static HashMap<String, Object> fromPurchaseHistoryRecord(
PurchaseHistoryRecord purchaseHistoryRecord) {
HashMap<String, Object> info = new HashMap<>();
List<String> skus = purchaseHistoryRecord.getSkus();
info.put("purchaseTime", purchaseHistoryRecord.getPurchaseTime());
info.put("purchaseToken", purchaseHistoryRecord.getPurchaseToken());
info.put("signature", purchaseHistoryRecord.getSignature());
info.put("sku", purchaseHistoryRecord.getSku());
info.put("skus", skus);
info.put("developerPayload", purchaseHistoryRecord.getDeveloperPayload());
info.put("originalJson", purchaseHistoryRecord.getOriginalJson());
info.put("quantity", purchaseHistoryRecord.getQuantity());
return info;
}

Expand Down Expand Up @@ -112,14 +115,6 @@ static List<HashMap<String, Object>> fromPurchaseHistoryRecordList(
return serialized;
}

static HashMap<String, Object> fromPurchasesResult(PurchasesResult purchasesResult) {
HashMap<String, Object> info = new HashMap<>();
info.put("responseCode", purchasesResult.getResponseCode());
info.put("billingResult", fromBillingResult(purchasesResult.getBillingResult()));
info.put("purchasesList", fromPurchasesList(purchasesResult.getPurchasesList()));
return info;
}

static HashMap<String, Object> fromBillingResult(BillingResult billingResult) {
HashMap<String, Object> info = new HashMap<>();
info.put("responseCode", billingResult.getResponseCode());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@
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_PURCHASES_ASYNC;
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.fromBillingResult;
import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList;
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.list;
import static java.util.Collections.singletonList;
import static java.util.Collections.unmodifiableList;
import static java.util.stream.Collectors.toList;
Expand Down Expand Up @@ -56,7 +57,6 @@
import com.android.billingclient.api.PriceChangeConfirmationListener;
import com.android.billingclient.api.PriceChangeFlowParams;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.Purchase.PurchasesResult;
import com.android.billingclient.api.PurchaseHistoryRecord;
import com.android.billingclient.api.PurchaseHistoryResponseListener;
import com.android.billingclient.api.SkuDetails;
Expand Down Expand Up @@ -294,7 +294,6 @@ public void launchBillingFlow_null_AccountId_do_not_crash() {
ArgumentCaptor.forClass(BillingFlowParams.class);
verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture());
BillingFlowParams params = billingFlowParamsCaptor.getValue();
assertEquals(params.getSku(), skuId);

// Verify we pass the response code to result
verify(result, never()).error(any(), any(), any());
Expand Down Expand Up @@ -327,8 +326,6 @@ public void launchBillingFlow_ok_null_OldSku() {
ArgumentCaptor.forClass(BillingFlowParams.class);
verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture());
BillingFlowParams params = billingFlowParamsCaptor.getValue();
assertEquals(params.getSku(), skuId);
assertNull(params.getOldSku());
// Verify we pass the response code to result
verify(result, never()).error(any(), any(), any());
verify(result, times(1)).success(fromBillingResult(billingResult));
Expand Down Expand Up @@ -380,8 +377,6 @@ public void launchBillingFlow_ok_oldSku() {
ArgumentCaptor.forClass(BillingFlowParams.class);
verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture());
BillingFlowParams params = billingFlowParamsCaptor.getValue();
assertEquals(params.getSku(), skuId);
assertEquals(params.getOldSku(), oldSkuId);

// Verify we pass the response code to result
verify(result, never()).error(any(), any(), any());
Expand Down Expand Up @@ -413,7 +408,6 @@ public void launchBillingFlow_ok_AccountId() {
ArgumentCaptor.forClass(BillingFlowParams.class);
verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture());
BillingFlowParams params = billingFlowParamsCaptor.getValue();
assertEquals(params.getSku(), skuId);

// Verify we pass the response code to result
verify(result, never()).error(any(), any(), any());
Expand Down Expand Up @@ -451,10 +445,6 @@ public void launchBillingFlow_ok_Proration() {
ArgumentCaptor.forClass(BillingFlowParams.class);
verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture());
BillingFlowParams params = billingFlowParamsCaptor.getValue();
assertEquals(params.getSku(), skuId);
assertEquals(params.getOldSku(), oldSkuId);
assertEquals(params.getOldSkuPurchaseToken(), purchaseToken);
assertEquals(params.getReplaceSkusProrationMode(), prorationMode);

// Verify we pass the response code to result
verify(result, never()).error(any(), any(), any());
Expand Down Expand Up @@ -495,6 +485,43 @@ public void launchBillingFlow_ok_Proration_with_null_OldSku() {
verify(result, never()).success(any());
}

@Test
public void launchBillingFlow_ok_Full() {
// Fetch the sku details first and query the method call
String skuId = "foo";
String oldSkuId = "oldFoo";
String purchaseToken = "purchaseTokenFoo";
String accountId = "account";
int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE;
queryForSkus(unmodifiableList(asList(skuId, oldSkuId)));
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("sku", skuId);
arguments.put("accountId", accountId);
arguments.put("oldSku", oldSkuId);
arguments.put("purchaseToken", purchaseToken);
arguments.put("prorationMode", prorationMode);
MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments);

// Launch the billing flow
BillingResult billingResult =
BillingResult.newBuilder()
.setResponseCode(100)
.setDebugMessage("dummy debug message")
.build();
when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult);
methodChannelHandler.onMethodCall(launchCall, result);

// Verify we pass the arguments to the billing flow
ArgumentCaptor<BillingFlowParams> billingFlowParamsCaptor =
ArgumentCaptor.forClass(BillingFlowParams.class);
verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture());
BillingFlowParams params = billingFlowParamsCaptor.getValue();

// Verify we pass the response code to result
verify(result, never()).error(any(), any(), any());
verify(result, times(1)).success(fromBillingResult(billingResult));
}

@Test
public void launchBillingFlow_clientDisconnected() {
// Prepare the launch call after disconnecting the client
Expand Down Expand Up @@ -553,39 +580,14 @@ public void launchBillingFlow_oldSkuNotFound() {
verify(result, never()).success(any());
}

@Test
public void queryPurchases() {
establishConnectedBillingClient(null, null);
PurchasesResult purchasesResult = mock(PurchasesResult.class);
Purchase purchase = buildPurchase("foo");
when(purchasesResult.getPurchasesList()).thenReturn(asList(purchase));
BillingResult billingResult =
BillingResult.newBuilder()
.setResponseCode(100)
.setDebugMessage("dummy debug message")
.build();
when(purchasesResult.getBillingResult()).thenReturn(billingResult);
when(mockBillingClient.queryPurchases(SkuType.INAPP)).thenReturn(purchasesResult);

HashMap<String, Object> arguments = new HashMap<>();
arguments.put("skuType", SkuType.INAPP);
methodChannelHandler.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
methodChannelHandler.onMethodCall(new MethodCall(END_CONNECTION, null), mock(Result.class));

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

// Assert that we sent an error back.
verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any());
Expand Down
Loading