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

[In_app_purchases] migrate to Play Billing Library 2.0. #2287

Merged
merged 21 commits into from
Jan 6, 2020
21 changes: 21 additions & 0 deletions packages/in_app_purchase/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
## 0.3.0

* Migrate the `Google Play Library` to 2.0.3.
* Introduce a new class `BillingResultWrapper` which contains a detailed result of a BillingClient operation.
* **[Breaking Change]:** All the BillingClient methods that previously return a `BillingResponse` now return a `BillingResultWrapper`, including: `launchBillingFlow`, `startConnection` and `consumeAsync`.
* **[Breaking Change]:** The `SkuDetailsResponseWrapper` now contains a `billingResult` field in place of `billingResponse` field.
* A `billingResult` field is added to the `PurchasesResultWrapper`.
* Other Updates to the "billing_client_wrappers":
* Updates to the `PurchaseWrapper`: Add `developerPayload`, `purchaseState` and `isAcknowledged` fields.
* Updates to the `SkuDetailsWrapper`: Add `originalPrice` and `originalPriceAmountMicros` fields.
* **[Breaking Change]:** The `BillingClient.queryPurchaseHistory` is updated to return a `PurchasesHistoryResult`, which contains a list of `PurchaseHistoryRecordWrapper` instead of `PurchaseWrapper`. A `PurchaseHistoryRecordWrapper` object has the same fields and values as A `PurchaseWrapper` object, except that a `PurchaseHistoryRecordWrapper` object does not contain `isAutoRenewing`, `orderId` and `packageName`.
* Add a new `BillingClient.acknowledgePurchase` API. Starting from this version, the developer has to acknowledge any purchase on Android using this API within 3 days of purchase, or the user will be refunded. Note that if a product is "consumed" via `BillingClient.consumeAsync`, it is implicitly acknowledged.
* **[Breaking Change]:** Added `enablePendingPurchases` in `BillingClientWrapper`. The application has to call this method before calling `BillingClientWrapper.startConnection`. See [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) for more information.
* Updates to the "InAppPurchaseConnection":
* **[Breaking Change]:** `InAppPurchaseConnection.completePurchase` now returns a `Future<BillingResultWrapper>` instead of `Future<void>`. A new optional parameter `{String developerPayload}` has also been added to the API. On Android, this API does not throw an exception anymore, it instead acknowledge the purchase. If a purchase is not completed within 3 days on Android, the user will be refunded.
* **[Breaking Change]:** `InAppPurchaseConnection.consumePurchase` now returns a `Future<BillingResultWrapper>` instead of `Future<BillingResponse>`. A new optional parameter `{String developerPayload}` has also been added to the API.
* A new boolean field `pendingCompletePurchase` has been added to the `PurchaseDetails` class. Which can be used as an indicator of whether to call `InAppPurchaseConnection.completePurchase` on the purchase.
* **[Breaking Change]:** Added `enablePendingPurchases` in `InAppPurchaseConnection`. The application has to call this method when initializing the `InAppPurchaseConnection` on Android. See [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) for more information.
* Misc: Some documentation updates reflecting the `BillingClient` migration and some documentation fixes.
* Refer to [Google Play Billing Library Release Note](https://developer.android.com/google/play/billing/billing_library_releases_notes#release-2_0) for a detailed information on the update.

## 0.2.2+6

* Correct a comment.
Expand Down
6 changes: 3 additions & 3 deletions packages/in_app_purchase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,15 @@ for (PurchaseDetails purchase in response.pastPurchases) {
}
```

Note that the App Store does not have any APIs for querying consummable
products, and Google Play considers consummable products to no longer be owned
Note that the App Store does not have any APIs for querying consumable
products, and Google Play considers consumable products to no longer be owned
once they're marked as consumed and fails to return them here. For restoring
these across devices you'll need to persist them on your own server and query
that as well.

### Making a purchase

Both storefronts handle consummable and non-consummable products differently. If
Both storefronts handle consumable and non-consumable products differently. If
you're using `InAppPurchaseConnection`, you need to make a distinction here and
call the right purchase method for each type.

Expand Down
2 changes: 1 addition & 1 deletion packages/in_app_purchase/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ android {

dependencies {
implementation 'androidx.annotation:annotation:1.0.0'
implementation 'com.android.billingclient:billing:1.2'
implementation 'com.android.billingclient:billing:2.0.3'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.17.0'
androidTestImplementation 'androidx.test:runner:1.1.1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ interface BillingClientFactory {
*
* @param context The context used to create the {@link BillingClient}.
* @param channel The method channel used to create the {@link BillingClient}.
* @param enablePendingPurchases Whether to enable pending purchases. Throws an exception if it is
* false.
* @return The {@link BillingClient} object that is created.
*/
BillingClient createBillingClient(@NonNull Context context, @NonNull MethodChannel channel);
BillingClient createBillingClient(
@NonNull Context context, @NonNull MethodChannel channel, boolean enablePendingPurchases);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
final class BillingClientFactoryImpl implements BillingClientFactory {

@Override
public BillingClient createBillingClient(Context context, MethodChannel channel) {
return BillingClient.newBuilder(context)
.setListener(new PluginPurchaseListener(channel))
.build();
public BillingClient createBillingClient(
Context context, MethodChannel channel, boolean enablePendingPurchases) {
BillingClient.Builder builder = BillingClient.newBuilder(context);
if (enablePendingPurchases) {
builder.enablePendingPurchases();
}
return builder.setListener(new PluginPurchaseListener(channel)).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ static final class MethodNames {
"BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)";
static final String CONSUME_PURCHASE_ASYNC =
"BillingClient#consumeAsync(String, ConsumeResponseListener)";
static final String ACKNOWLEDGE_PURCHASE =
"BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)";

private MethodNames() {};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

package io.flutter.plugins.inapppurchase;

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

Expand All @@ -13,11 +13,15 @@
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.billingclient.api.AcknowledgePurchaseParams;
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchaseHistoryRecord;
import com.android.billingclient.api.PurchaseHistoryResponseListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
Expand Down Expand Up @@ -69,7 +73,10 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
isReady(result);
break;
case InAppPurchasePlugin.MethodNames.START_CONNECTION:
startConnection((int) call.argument("handle"), result);
startConnection(
(int) call.argument("handle"),
(boolean) call.argument("enablePendingPurchases"),
result);
break;
case InAppPurchasePlugin.MethodNames.END_CONNECTION:
endConnection(result);
Expand All @@ -89,7 +96,16 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
queryPurchaseHistoryAsync((String) call.argument("skuType"), result);
break;
case InAppPurchasePlugin.MethodNames.CONSUME_PURCHASE_ASYNC:
consumeAsync((String) call.argument("purchaseToken"), result);
consumeAsync(
(String) call.argument("purchaseToken"),
(String) call.argument("developerPayload"),
result);
break;
case InAppPurchasePlugin.MethodNames.ACKNOWLEDGE_PURCHASE:
acknowledgePurchase(
(String) call.argument("purchaseToken"),
(String) call.argument("developerPayload"),
result);
break;
default:
result.notImplemented();
Expand Down Expand Up @@ -123,11 +139,12 @@ private void querySkuDetailsAsync(
billingClient.querySkuDetailsAsync(
params,
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(
int responseCode, @Nullable List<SkuDetails> skuDetailsList) {
BillingResult billingResult, List<SkuDetails> skuDetailsList) {
updateCachedSkus(skuDetailsList);
final Map<String, Object> skuDetailsResponse = new HashMap<>();
skuDetailsResponse.put("responseCode", responseCode);
skuDetailsResponse.put("billingResult", Translator.fromBillingResult(billingResult));
skuDetailsResponse.put("skuDetailsList", fromSkuDetailsList(skuDetailsList));
result.success(skuDetailsResponse);
}
Expand Down Expand Up @@ -164,23 +181,33 @@ private void launchBillingFlow(
if (accountId != null && !accountId.isEmpty()) {
paramsBuilder.setAccountId(accountId);
}
result.success(billingClient.launchBillingFlow(activity, paramsBuilder.build()));
result.success(
Translator.fromBillingResult(
billingClient.launchBillingFlow(activity, paramsBuilder.build())));
}

private void consumeAsync(String purchaseToken, final MethodChannel.Result result) {
private void consumeAsync(
String purchaseToken, String developerPayload, final MethodChannel.Result result) {
if (billingClientError(result)) {
return;
}

ConsumeResponseListener listener =
new ConsumeResponseListener() {
@Override
public void onConsumeResponse(
@BillingClient.BillingResponse int responseCode, String outToken) {
result.success(responseCode);
public void onConsumeResponse(BillingResult billingResult, String outToken) {
result.success(Translator.fromBillingResult(billingResult));
}
};
billingClient.consumeAsync(purchaseToken, listener);
ConsumeParams.Builder paramsBuilder =
ConsumeParams.newBuilder().setPurchaseToken(purchaseToken);

if (developerPayload != null) {
paramsBuilder.setDeveloperPayload(developerPayload);
}
ConsumeParams params = paramsBuilder.build();

billingClient.consumeAsync(params, listener);
}

private void queryPurchases(String skuType, MethodChannel.Result result) {
Expand All @@ -201,33 +228,38 @@ private void queryPurchaseHistoryAsync(String skuType, final MethodChannel.Resul
skuType,
new PurchaseHistoryResponseListener() {
@Override
public void onPurchaseHistoryResponse(int responseCode, List<Purchase> purchasesList) {
public void onPurchaseHistoryResponse(
BillingResult billingResult, List<PurchaseHistoryRecord> purchasesList) {
final Map<String, Object> serialized = new HashMap<>();
serialized.put("responseCode", responseCode);
serialized.put("purchasesList", fromPurchasesList(purchasesList));
serialized.put("billingResult", Translator.fromBillingResult(billingResult));
serialized.put(
"purchaseHistoryRecordList", fromPurchaseHistoryRecordList(purchasesList));
result.success(serialized);
}
});
}

private void startConnection(final int handle, final MethodChannel.Result result) {
private void startConnection(
final int handle, final boolean enablePendingPurchases, final MethodChannel.Result result) {
if (billingClient == null) {
billingClient = billingClientFactory.createBillingClient(applicationContext, methodChannel);
billingClient =
billingClientFactory.createBillingClient(
applicationContext, methodChannel, enablePendingPurchases);
}

billingClient.startConnection(
new BillingClientStateListener() {
private boolean alreadyFinished = false;

@Override
public void onBillingSetupFinished(int responseCode) {
public void onBillingSetupFinished(BillingResult billingResult) {
if (alreadyFinished) {
Log.d(TAG, "Tried to call onBilllingSetupFinished multiple times.");
return;
}
alreadyFinished = true;
// Consider the fact that we've finished a success, leave it to the Dart side to validate the responseCode.
result.success(responseCode);
result.success(Translator.fromBillingResult(billingResult));
}

@Override
Expand All @@ -239,6 +271,26 @@ public void onBillingServiceDisconnected() {
});
}

private void acknowledgePurchase(
String purchaseToken, @Nullable String developerPayload, final MethodChannel.Result result) {
if (billingClientError(result)) {
return;
}
AcknowledgePurchaseParams params =
AcknowledgePurchaseParams.newBuilder()
.setDeveloperPayload(developerPayload)
.setPurchaseToken(purchaseToken)
.build();
billingClient.acknowledgePurchase(
params,
new AcknowledgePurchaseResponseListener() {
@Override
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
result.success(Translator.fromBillingResult(billingResult));
}
});
}

private void updateCachedSkus(@Nullable List<SkuDetails> skuDetailsList) {
if (skuDetailsList == null) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

package io.flutter.plugins.inapppurchase;

import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult;
import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList;

import androidx.annotation.Nullable;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import io.flutter.plugin.common.MethodChannel;
Expand All @@ -22,9 +24,10 @@ class PluginPurchaseListener implements PurchasesUpdatedListener {
}

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

import androidx.annotation.Nullable;
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;
import java.util.Collections;
Expand All @@ -31,6 +33,8 @@ static HashMap<String, Object> fromSkuDetail(SkuDetails detail) {
info.put("type", detail.getType());
info.put("isRewarded", detail.isRewarded());
info.put("subscriptionPeriod", detail.getSubscriptionPeriod());
info.put("originalPrice", detail.getOriginalPrice());
info.put("originalPriceAmountMicros", detail.getOriginalPriceAmountMicros());
return info;
}

Expand All @@ -57,6 +61,21 @@ static HashMap<String, Object> fromPurchase(Purchase purchase) {
info.put("sku", purchase.getSku());
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());
return info;
}

static HashMap<String, Object> fromPurchaseHistoryRecord(
PurchaseHistoryRecord purchaseHistoryRecord) {
HashMap<String, Object> info = new HashMap<>();
info.put("purchaseTime", purchaseHistoryRecord.getPurchaseTime());
info.put("purchaseToken", purchaseHistoryRecord.getPurchaseToken());
info.put("signature", purchaseHistoryRecord.getSignature());
info.put("sku", purchaseHistoryRecord.getSku());
info.put("developerPayload", purchaseHistoryRecord.getDeveloperPayload());
info.put("originalJson", purchaseHistoryRecord.getOriginalJson());
return info;
}

Expand All @@ -72,10 +91,31 @@ static List<HashMap<String, Object>> fromPurchasesList(@Nullable List<Purchase>
return serialized;
}

static List<HashMap<String, Object>> fromPurchaseHistoryRecordList(
@Nullable List<PurchaseHistoryRecord> purchaseHistoryRecords) {
if (purchaseHistoryRecords == null) {
return Collections.emptyList();
}

List<HashMap<String, Object>> serialized = new ArrayList<>();
for (PurchaseHistoryRecord purchaseHistoryRecord : purchaseHistoryRecords) {
serialized.add(fromPurchaseHistoryRecord(purchaseHistoryRecord));
}
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());
info.put("debugMessage", billingResult.getDebugMessage());
return info;
}
}
Loading