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

Commit ed7620c

Browse files
author
Chris Yang
authored
[In_app_purchases] migrate to Play Billing Library 2.0. (#2287)
1 parent 0a9d0c5 commit ed7620c

30 files changed

+1380
-389
lines changed

packages/in_app_purchase/CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
## 0.3.0
2+
3+
* Migrate the `Google Play Library` to 2.0.3.
4+
* Introduce a new class `BillingResultWrapper` which contains a detailed result of a BillingClient operation.
5+
* **[Breaking Change]:** All the BillingClient methods that previously return a `BillingResponse` now return a `BillingResultWrapper`, including: `launchBillingFlow`, `startConnection` and `consumeAsync`.
6+
* **[Breaking Change]:** The `SkuDetailsResponseWrapper` now contains a `billingResult` field in place of `billingResponse` field.
7+
* A `billingResult` field is added to the `PurchasesResultWrapper`.
8+
* Other Updates to the "billing_client_wrappers":
9+
* Updates to the `PurchaseWrapper`: Add `developerPayload`, `purchaseState` and `isAcknowledged` fields.
10+
* Updates to the `SkuDetailsWrapper`: Add `originalPrice` and `originalPriceAmountMicros` fields.
11+
* **[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`.
12+
* 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.
13+
* **[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.
14+
* Updates to the "InAppPurchaseConnection":
15+
* **[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.
16+
* **[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.
17+
* 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.
18+
* **[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.
19+
* Misc: Some documentation updates reflecting the `BillingClient` migration and some documentation fixes.
20+
* 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.
21+
122
## 0.2.2+6
223

324
* Correct a comment.

packages/in_app_purchase/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,15 @@ for (PurchaseDetails purchase in response.pastPurchases) {
114114
}
115115
```
116116

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

123123
### Making a purchase
124124

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

packages/in_app_purchase/android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ android {
3535

3636
dependencies {
3737
implementation 'androidx.annotation:annotation:1.0.0'
38-
implementation 'com.android.billingclient:billing:1.2'
38+
implementation 'com.android.billingclient:billing:2.0.3'
3939
testImplementation 'junit:junit:4.12'
4040
testImplementation 'org.mockito:mockito-core:2.17.0'
4141
androidTestImplementation 'androidx.test:runner:1.1.1'

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ interface BillingClientFactory {
1717
*
1818
* @param context The context used to create the {@link BillingClient}.
1919
* @param channel The method channel used to create the {@link BillingClient}.
20+
* @param enablePendingPurchases Whether to enable pending purchases. Throws an exception if it is
21+
* false.
2022
* @return The {@link BillingClient} object that is created.
2123
*/
22-
BillingClient createBillingClient(@NonNull Context context, @NonNull MethodChannel channel);
24+
BillingClient createBillingClient(
25+
@NonNull Context context, @NonNull MethodChannel channel, boolean enablePendingPurchases);
2326
}

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212
final class BillingClientFactoryImpl implements BillingClientFactory {
1313

1414
@Override
15-
public BillingClient createBillingClient(Context context, MethodChannel channel) {
16-
return BillingClient.newBuilder(context)
17-
.setListener(new PluginPurchaseListener(channel))
18-
.build();
15+
public BillingClient createBillingClient(
16+
Context context, MethodChannel channel, boolean enablePendingPurchases) {
17+
BillingClient.Builder builder = BillingClient.newBuilder(context);
18+
if (enablePendingPurchases) {
19+
builder.enablePendingPurchases();
20+
}
21+
return builder.setListener(new PluginPurchaseListener(channel)).build();
1922
}
2023
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ static final class MethodNames {
3636
"BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)";
3737
static final String CONSUME_PURCHASE_ASYNC =
3838
"BillingClient#consumeAsync(String, ConsumeResponseListener)";
39+
static final String ACKNOWLEDGE_PURCHASE =
40+
"BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)";
3941

4042
private MethodNames() {};
4143
}

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

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
package io.flutter.plugins.inapppurchase;
66

7-
import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList;
7+
import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList;
88
import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesResult;
99
import static io.flutter.plugins.inapppurchase.Translator.fromSkuDetailsList;
1010

@@ -13,11 +13,15 @@
1313
import android.util.Log;
1414
import androidx.annotation.NonNull;
1515
import androidx.annotation.Nullable;
16+
import com.android.billingclient.api.AcknowledgePurchaseParams;
17+
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
1618
import com.android.billingclient.api.BillingClient;
1719
import com.android.billingclient.api.BillingClientStateListener;
1820
import com.android.billingclient.api.BillingFlowParams;
21+
import com.android.billingclient.api.BillingResult;
22+
import com.android.billingclient.api.ConsumeParams;
1923
import com.android.billingclient.api.ConsumeResponseListener;
20-
import com.android.billingclient.api.Purchase;
24+
import com.android.billingclient.api.PurchaseHistoryRecord;
2125
import com.android.billingclient.api.PurchaseHistoryResponseListener;
2226
import com.android.billingclient.api.SkuDetails;
2327
import com.android.billingclient.api.SkuDetailsParams;
@@ -69,7 +73,10 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
6973
isReady(result);
7074
break;
7175
case InAppPurchasePlugin.MethodNames.START_CONNECTION:
72-
startConnection((int) call.argument("handle"), result);
76+
startConnection(
77+
(int) call.argument("handle"),
78+
(boolean) call.argument("enablePendingPurchases"),
79+
result);
7380
break;
7481
case InAppPurchasePlugin.MethodNames.END_CONNECTION:
7582
endConnection(result);
@@ -89,7 +96,16 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
8996
queryPurchaseHistoryAsync((String) call.argument("skuType"), result);
9097
break;
9198
case InAppPurchasePlugin.MethodNames.CONSUME_PURCHASE_ASYNC:
92-
consumeAsync((String) call.argument("purchaseToken"), result);
99+
consumeAsync(
100+
(String) call.argument("purchaseToken"),
101+
(String) call.argument("developerPayload"),
102+
result);
103+
break;
104+
case InAppPurchasePlugin.MethodNames.ACKNOWLEDGE_PURCHASE:
105+
acknowledgePurchase(
106+
(String) call.argument("purchaseToken"),
107+
(String) call.argument("developerPayload"),
108+
result);
93109
break;
94110
default:
95111
result.notImplemented();
@@ -123,11 +139,12 @@ private void querySkuDetailsAsync(
123139
billingClient.querySkuDetailsAsync(
124140
params,
125141
new SkuDetailsResponseListener() {
142+
@Override
126143
public void onSkuDetailsResponse(
127-
int responseCode, @Nullable List<SkuDetails> skuDetailsList) {
144+
BillingResult billingResult, List<SkuDetails> skuDetailsList) {
128145
updateCachedSkus(skuDetailsList);
129146
final Map<String, Object> skuDetailsResponse = new HashMap<>();
130-
skuDetailsResponse.put("responseCode", responseCode);
147+
skuDetailsResponse.put("billingResult", Translator.fromBillingResult(billingResult));
131148
skuDetailsResponse.put("skuDetailsList", fromSkuDetailsList(skuDetailsList));
132149
result.success(skuDetailsResponse);
133150
}
@@ -164,23 +181,33 @@ private void launchBillingFlow(
164181
if (accountId != null && !accountId.isEmpty()) {
165182
paramsBuilder.setAccountId(accountId);
166183
}
167-
result.success(billingClient.launchBillingFlow(activity, paramsBuilder.build()));
184+
result.success(
185+
Translator.fromBillingResult(
186+
billingClient.launchBillingFlow(activity, paramsBuilder.build())));
168187
}
169188

170-
private void consumeAsync(String purchaseToken, final MethodChannel.Result result) {
189+
private void consumeAsync(
190+
String purchaseToken, String developerPayload, final MethodChannel.Result result) {
171191
if (billingClientError(result)) {
172192
return;
173193
}
174194

175195
ConsumeResponseListener listener =
176196
new ConsumeResponseListener() {
177197
@Override
178-
public void onConsumeResponse(
179-
@BillingClient.BillingResponse int responseCode, String outToken) {
180-
result.success(responseCode);
198+
public void onConsumeResponse(BillingResult billingResult, String outToken) {
199+
result.success(Translator.fromBillingResult(billingResult));
181200
}
182201
};
183-
billingClient.consumeAsync(purchaseToken, listener);
202+
ConsumeParams.Builder paramsBuilder =
203+
ConsumeParams.newBuilder().setPurchaseToken(purchaseToken);
204+
205+
if (developerPayload != null) {
206+
paramsBuilder.setDeveloperPayload(developerPayload);
207+
}
208+
ConsumeParams params = paramsBuilder.build();
209+
210+
billingClient.consumeAsync(params, listener);
184211
}
185212

186213
private void queryPurchases(String skuType, MethodChannel.Result result) {
@@ -201,33 +228,38 @@ private void queryPurchaseHistoryAsync(String skuType, final MethodChannel.Resul
201228
skuType,
202229
new PurchaseHistoryResponseListener() {
203230
@Override
204-
public void onPurchaseHistoryResponse(int responseCode, List<Purchase> purchasesList) {
231+
public void onPurchaseHistoryResponse(
232+
BillingResult billingResult, List<PurchaseHistoryRecord> purchasesList) {
205233
final Map<String, Object> serialized = new HashMap<>();
206-
serialized.put("responseCode", responseCode);
207-
serialized.put("purchasesList", fromPurchasesList(purchasesList));
234+
serialized.put("billingResult", Translator.fromBillingResult(billingResult));
235+
serialized.put(
236+
"purchaseHistoryRecordList", fromPurchaseHistoryRecordList(purchasesList));
208237
result.success(serialized);
209238
}
210239
});
211240
}
212241

213-
private void startConnection(final int handle, final MethodChannel.Result result) {
242+
private void startConnection(
243+
final int handle, final boolean enablePendingPurchases, final MethodChannel.Result result) {
214244
if (billingClient == null) {
215-
billingClient = billingClientFactory.createBillingClient(applicationContext, methodChannel);
245+
billingClient =
246+
billingClientFactory.createBillingClient(
247+
applicationContext, methodChannel, enablePendingPurchases);
216248
}
217249

218250
billingClient.startConnection(
219251
new BillingClientStateListener() {
220252
private boolean alreadyFinished = false;
221253

222254
@Override
223-
public void onBillingSetupFinished(int responseCode) {
255+
public void onBillingSetupFinished(BillingResult billingResult) {
224256
if (alreadyFinished) {
225257
Log.d(TAG, "Tried to call onBilllingSetupFinished multiple times.");
226258
return;
227259
}
228260
alreadyFinished = true;
229261
// Consider the fact that we've finished a success, leave it to the Dart side to validate the responseCode.
230-
result.success(responseCode);
262+
result.success(Translator.fromBillingResult(billingResult));
231263
}
232264

233265
@Override
@@ -239,6 +271,26 @@ public void onBillingServiceDisconnected() {
239271
});
240272
}
241273

274+
private void acknowledgePurchase(
275+
String purchaseToken, @Nullable String developerPayload, final MethodChannel.Result result) {
276+
if (billingClientError(result)) {
277+
return;
278+
}
279+
AcknowledgePurchaseParams params =
280+
AcknowledgePurchaseParams.newBuilder()
281+
.setDeveloperPayload(developerPayload)
282+
.setPurchaseToken(purchaseToken)
283+
.build();
284+
billingClient.acknowledgePurchase(
285+
params,
286+
new AcknowledgePurchaseResponseListener() {
287+
@Override
288+
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
289+
result.success(Translator.fromBillingResult(billingResult));
290+
}
291+
});
292+
}
293+
242294
private void updateCachedSkus(@Nullable List<SkuDetails> skuDetailsList) {
243295
if (skuDetailsList == null) {
244296
return;

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

Lines changed: 5 additions & 2 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.fromBillingResult;
78
import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList;
89

910
import androidx.annotation.Nullable;
11+
import com.android.billingclient.api.BillingResult;
1012
import com.android.billingclient.api.Purchase;
1113
import com.android.billingclient.api.PurchasesUpdatedListener;
1214
import io.flutter.plugin.common.MethodChannel;
@@ -22,9 +24,10 @@ class PluginPurchaseListener implements PurchasesUpdatedListener {
2224
}
2325

2426
@Override
25-
public void onPurchasesUpdated(int responseCode, @Nullable List<Purchase> purchases) {
27+
public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> purchases) {
2628
final Map<String, Object> callbackArgs = new HashMap<>();
27-
callbackArgs.put("responseCode", responseCode);
29+
callbackArgs.put("billingResult", fromBillingResult(billingResult));
30+
callbackArgs.put("responseCode", billingResult.getResponseCode());
2831
callbackArgs.put("purchasesList", fromPurchasesList(purchases));
2932
channel.invokeMethod(InAppPurchasePlugin.MethodNames.ON_PURCHASES_UPDATED, callbackArgs);
3033
}

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
package io.flutter.plugins.inapppurchase;
66

77
import androidx.annotation.Nullable;
8+
import com.android.billingclient.api.BillingResult;
89
import com.android.billingclient.api.Purchase;
910
import com.android.billingclient.api.Purchase.PurchasesResult;
11+
import com.android.billingclient.api.PurchaseHistoryRecord;
1012
import com.android.billingclient.api.SkuDetails;
1113
import java.util.ArrayList;
1214
import java.util.Collections;
@@ -31,6 +33,8 @@ static HashMap<String, Object> fromSkuDetail(SkuDetails detail) {
3133
info.put("type", detail.getType());
3234
info.put("isRewarded", detail.isRewarded());
3335
info.put("subscriptionPeriod", detail.getSubscriptionPeriod());
36+
info.put("originalPrice", detail.getOriginalPrice());
37+
info.put("originalPriceAmountMicros", detail.getOriginalPriceAmountMicros());
3438
return info;
3539
}
3640

@@ -57,6 +61,21 @@ static HashMap<String, Object> fromPurchase(Purchase purchase) {
5761
info.put("sku", purchase.getSku());
5862
info.put("isAutoRenewing", purchase.isAutoRenewing());
5963
info.put("originalJson", purchase.getOriginalJson());
64+
info.put("developerPayload", purchase.getDeveloperPayload());
65+
info.put("isAcknowledged", purchase.isAcknowledged());
66+
info.put("purchaseState", purchase.getPurchaseState());
67+
return info;
68+
}
69+
70+
static HashMap<String, Object> fromPurchaseHistoryRecord(
71+
PurchaseHistoryRecord purchaseHistoryRecord) {
72+
HashMap<String, Object> info = new HashMap<>();
73+
info.put("purchaseTime", purchaseHistoryRecord.getPurchaseTime());
74+
info.put("purchaseToken", purchaseHistoryRecord.getPurchaseToken());
75+
info.put("signature", purchaseHistoryRecord.getSignature());
76+
info.put("sku", purchaseHistoryRecord.getSku());
77+
info.put("developerPayload", purchaseHistoryRecord.getDeveloperPayload());
78+
info.put("originalJson", purchaseHistoryRecord.getOriginalJson());
6079
return info;
6180
}
6281

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

94+
static List<HashMap<String, Object>> fromPurchaseHistoryRecordList(
95+
@Nullable List<PurchaseHistoryRecord> purchaseHistoryRecords) {
96+
if (purchaseHistoryRecords == null) {
97+
return Collections.emptyList();
98+
}
99+
100+
List<HashMap<String, Object>> serialized = new ArrayList<>();
101+
for (PurchaseHistoryRecord purchaseHistoryRecord : purchaseHistoryRecords) {
102+
serialized.add(fromPurchaseHistoryRecord(purchaseHistoryRecord));
103+
}
104+
return serialized;
105+
}
106+
75107
static HashMap<String, Object> fromPurchasesResult(PurchasesResult purchasesResult) {
76108
HashMap<String, Object> info = new HashMap<>();
77109
info.put("responseCode", purchasesResult.getResponseCode());
110+
info.put("billingResult", fromBillingResult(purchasesResult.getBillingResult()));
78111
info.put("purchasesList", fromPurchasesList(purchasesResult.getPurchasesList()));
79112
return info;
80113
}
114+
115+
static HashMap<String, Object> fromBillingResult(BillingResult billingResult) {
116+
HashMap<String, Object> info = new HashMap<>();
117+
info.put("responseCode", billingResult.getResponseCode());
118+
info.put("debugMessage", billingResult.getDebugMessage());
119+
return info;
120+
}
81121
}

0 commit comments

Comments
 (0)