Skip to content

Commit dc5ff42

Browse files
authored
[in_app_purchase] Fully migrate to BillingClient V5 (#3752)
Sunsets BillingClient v4 calls in favor of the new v5 calls. Changes mostly follow the [Migration guide](https://developer.android.com/google/play/billing/migrate-gpblv5). Besides solving several issues, this change is also required as [billing client v4 will be obsolete by August 2nd, 2023](https://developer.android.com/google/play/billing/deprecation-faq). `getProductDetails()` will now return both base plans and their offers for subscriptions. Before, it would only return subscriptions that were backwards compatible with billing client v4. Price changes for subscriptions seem to be handled by the Play Store now instead. Therefore, `launchPriceChangeConfirmationFlow()` has been removed, making this PR a breaking change. Context: * [Billing Client API](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#launchPriceChangeConfirmationFlow(android.app.Activity,%20com.android.billingclient.api.PriceChangeFlowParams,%20com.android.billingclient.api.PriceChangeConfirmationListener)) * [Billing Client docs](https://developer.android.com/google/play/billing/subscriptions#price-change) This PR fixes the following issues: * Fixes [#110909](flutter/flutter#110909) * Fixes [#107370](flutter/flutter#107370) * Fixes [#114265](flutter/flutter#114265)
1 parent 2b38236 commit dc5ff42

37 files changed

+2076
-1482
lines changed

packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.3.0
2+
* **BREAKING CHANGE**: Removes `launchPriceChangeConfirmationFlow` from `InAppPurchaseAndroidPlatform`. Price changes are now [handled by Google Play](https://developer.android.com/google/play/billing/subscriptions#price-change).
3+
* Returns both base plans and offers when `queryProductDetailsAsync` is called.
4+
15
## 0.2.5+5
26

37
* Updates gradle, AGP and fixes some lint errors.

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

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,23 +32,21 @@ static final class MethodNames {
3232
"BillingClient#startConnection(BillingClientStateListener)";
3333
static final String END_CONNECTION = "BillingClient#endConnection()";
3434
static final String ON_DISCONNECT = "BillingClientStateListener#onBillingServiceDisconnected()";
35-
static final String QUERY_SKU_DETAILS =
36-
"BillingClient#querySkuDetailsAsync(SkuDetailsParams, SkuDetailsResponseListener)";
35+
static final String QUERY_PRODUCT_DETAILS =
36+
"BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener)";
3737
static final String LAUNCH_BILLING_FLOW =
3838
"BillingClient#launchBillingFlow(Activity, BillingFlowParams)";
3939
static final String ON_PURCHASES_UPDATED =
40-
"PurchasesUpdatedListener#onPurchasesUpdated(int, List<Purchase>)";
41-
static final String QUERY_PURCHASES = "BillingClient#queryPurchases(String)";
42-
static final String QUERY_PURCHASES_ASYNC = "BillingClient#queryPurchasesAsync(String)";
40+
"PurchasesUpdatedListener#onPurchasesUpdated(BillingResult, List<Purchase>)";
41+
static final String QUERY_PURCHASES_ASYNC =
42+
"BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)";
4343
static final String QUERY_PURCHASE_HISTORY_ASYNC =
44-
"BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)";
44+
"BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener)";
4545
static final String CONSUME_PURCHASE_ASYNC =
46-
"BillingClient#consumeAsync(String, ConsumeResponseListener)";
46+
"BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener)";
4747
static final String ACKNOWLEDGE_PURCHASE =
48-
"BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)";
48+
"BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)";
4949
static final String IS_FEATURE_SUPPORTED = "BillingClient#isFeatureSupported(String)";
50-
static final String LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW =
51-
"BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)";
5250
static final String GET_CONNECTION_STATE = "BillingClient#getConnectionState()";
5351

5452
private MethodNames() {};

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

Lines changed: 105 additions & 127 deletions
Large diffs are not rendered by default.

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

Lines changed: 135 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,69 +4,173 @@
44

55
package io.flutter.plugins.inapppurchase;
66

7+
import androidx.annotation.NonNull;
78
import androidx.annotation.Nullable;
89
import com.android.billingclient.api.AccountIdentifiers;
910
import com.android.billingclient.api.BillingResult;
11+
import com.android.billingclient.api.ProductDetails;
1012
import com.android.billingclient.api.Purchase;
1113
import com.android.billingclient.api.PurchaseHistoryRecord;
14+
import com.android.billingclient.api.QueryProductDetailsParams;
1215
import java.util.ArrayList;
1316
import java.util.Collections;
1417
import java.util.Currency;
1518
import java.util.HashMap;
1619
import java.util.List;
1720
import java.util.Locale;
21+
import java.util.Map;
1822

19-
/** Handles serialization of {@link com.android.billingclient.api.BillingClient} related objects. */
23+
/**
24+
* Handles serialization and deserialization of {@link com.android.billingclient.api.BillingClient}
25+
* related objects.
26+
*/
2027
/*package*/ class Translator {
21-
// TODO(stuartmorgan): Migrate this code. See TODO on MethodCallHandlerImpl.querySkuDetailsAsync.
22-
@SuppressWarnings("deprecation")
23-
static HashMap<String, Object> fromSkuDetail(com.android.billingclient.api.SkuDetails detail) {
28+
static HashMap<String, Object> fromProductDetail(ProductDetails detail) {
2429
HashMap<String, Object> info = new HashMap<>();
2530
info.put("title", detail.getTitle());
2631
info.put("description", detail.getDescription());
27-
info.put("freeTrialPeriod", detail.getFreeTrialPeriod());
28-
info.put("introductoryPrice", detail.getIntroductoryPrice());
29-
info.put("introductoryPriceAmountMicros", detail.getIntroductoryPriceAmountMicros());
30-
info.put("introductoryPriceCycles", detail.getIntroductoryPriceCycles());
31-
info.put("introductoryPricePeriod", detail.getIntroductoryPricePeriod());
32-
info.put("price", detail.getPrice());
33-
info.put("priceAmountMicros", detail.getPriceAmountMicros());
34-
info.put("priceCurrencyCode", detail.getPriceCurrencyCode());
35-
info.put("priceCurrencySymbol", currencySymbolFromCode(detail.getPriceCurrencyCode()));
36-
info.put("sku", detail.getSku());
37-
info.put("type", detail.getType());
38-
info.put("subscriptionPeriod", detail.getSubscriptionPeriod());
39-
info.put("originalPrice", detail.getOriginalPrice());
40-
info.put("originalPriceAmountMicros", detail.getOriginalPriceAmountMicros());
32+
info.put("productId", detail.getProductId());
33+
info.put("productType", detail.getProductType());
34+
info.put("name", detail.getName());
35+
36+
@Nullable
37+
ProductDetails.OneTimePurchaseOfferDetails oneTimePurchaseOfferDetails =
38+
detail.getOneTimePurchaseOfferDetails();
39+
if (oneTimePurchaseOfferDetails != null) {
40+
info.put(
41+
"oneTimePurchaseOfferDetails",
42+
fromOneTimePurchaseOfferDetails(oneTimePurchaseOfferDetails));
43+
}
44+
45+
@Nullable
46+
List<ProductDetails.SubscriptionOfferDetails> subscriptionOfferDetailsList =
47+
detail.getSubscriptionOfferDetails();
48+
if (subscriptionOfferDetailsList != null) {
49+
info.put(
50+
"subscriptionOfferDetails",
51+
fromSubscriptionOfferDetailsList(subscriptionOfferDetailsList));
52+
}
53+
4154
return info;
4255
}
4356

44-
// TODO(stuartmorgan): Migrate this code. See TODO on MethodCallHandlerImpl.querySkuDetailsAsync.
45-
@SuppressWarnings("deprecation")
46-
static List<HashMap<String, Object>> fromSkuDetailsList(
47-
@Nullable List<com.android.billingclient.api.SkuDetails> skuDetailsList) {
48-
if (skuDetailsList == null) {
57+
static List<QueryProductDetailsParams.Product> toProductList(List<Object> serialized) {
58+
List<QueryProductDetailsParams.Product> products = new ArrayList<>();
59+
for (Object productSerialized : serialized) {
60+
@SuppressWarnings(value = "unchecked")
61+
Map<String, Object> productMap = (Map<String, Object>) productSerialized;
62+
products.add(toProduct(productMap));
63+
}
64+
return products;
65+
}
66+
67+
static QueryProductDetailsParams.Product toProduct(Map<String, Object> serialized) {
68+
String productId = (String) serialized.get("productId");
69+
String productType = (String) serialized.get("productType");
70+
return QueryProductDetailsParams.Product.newBuilder()
71+
.setProductId(productId)
72+
.setProductType(productType)
73+
.build();
74+
}
75+
76+
static List<HashMap<String, Object>> fromProductDetailsList(
77+
@Nullable List<ProductDetails> productDetailsList) {
78+
if (productDetailsList == null) {
4979
return Collections.emptyList();
5080
}
5181

5282
ArrayList<HashMap<String, Object>> output = new ArrayList<>();
53-
for (com.android.billingclient.api.SkuDetails detail : skuDetailsList) {
54-
output.add(fromSkuDetail(detail));
83+
for (ProductDetails detail : productDetailsList) {
84+
output.add(fromProductDetail(detail));
5585
}
5686
return output;
5787
}
5888

89+
static HashMap<String, Object> fromOneTimePurchaseOfferDetails(
90+
@Nullable ProductDetails.OneTimePurchaseOfferDetails oneTimePurchaseOfferDetails) {
91+
HashMap<String, Object> serialized = new HashMap<>();
92+
if (oneTimePurchaseOfferDetails == null) {
93+
return serialized;
94+
}
95+
96+
serialized.put("priceAmountMicros", oneTimePurchaseOfferDetails.getPriceAmountMicros());
97+
serialized.put("priceCurrencyCode", oneTimePurchaseOfferDetails.getPriceCurrencyCode());
98+
serialized.put("formattedPrice", oneTimePurchaseOfferDetails.getFormattedPrice());
99+
100+
return serialized;
101+
}
102+
103+
static List<HashMap<String, Object>> fromSubscriptionOfferDetailsList(
104+
@Nullable List<ProductDetails.SubscriptionOfferDetails> subscriptionOfferDetailsList) {
105+
if (subscriptionOfferDetailsList == null) {
106+
return Collections.emptyList();
107+
}
108+
109+
ArrayList<HashMap<String, Object>> serialized = new ArrayList<>();
110+
111+
for (ProductDetails.SubscriptionOfferDetails subscriptionOfferDetails :
112+
subscriptionOfferDetailsList) {
113+
serialized.add(fromSubscriptionOfferDetails(subscriptionOfferDetails));
114+
}
115+
116+
return serialized;
117+
}
118+
119+
static HashMap<String, Object> fromSubscriptionOfferDetails(
120+
@Nullable ProductDetails.SubscriptionOfferDetails subscriptionOfferDetails) {
121+
HashMap<String, Object> serialized = new HashMap<>();
122+
if (subscriptionOfferDetails == null) {
123+
return serialized;
124+
}
125+
126+
serialized.put("offerId", subscriptionOfferDetails.getOfferId());
127+
serialized.put("basePlanId", subscriptionOfferDetails.getBasePlanId());
128+
serialized.put("offerTags", subscriptionOfferDetails.getOfferTags());
129+
serialized.put("offerIdToken", subscriptionOfferDetails.getOfferToken());
130+
131+
ProductDetails.PricingPhases pricingPhases = subscriptionOfferDetails.getPricingPhases();
132+
serialized.put("pricingPhases", fromPricingPhases(pricingPhases));
133+
134+
return serialized;
135+
}
136+
137+
static List<HashMap<String, Object>> fromPricingPhases(
138+
@NonNull ProductDetails.PricingPhases pricingPhases) {
139+
ArrayList<HashMap<String, Object>> serialized = new ArrayList<>();
140+
141+
for (ProductDetails.PricingPhase pricingPhase : pricingPhases.getPricingPhaseList()) {
142+
serialized.add(fromPricingPhase(pricingPhase));
143+
}
144+
return serialized;
145+
}
146+
147+
static HashMap<String, Object> fromPricingPhase(
148+
@Nullable ProductDetails.PricingPhase pricingPhase) {
149+
HashMap<String, Object> serialized = new HashMap<>();
150+
151+
if (pricingPhase == null) {
152+
return serialized;
153+
}
154+
155+
serialized.put("formattedPrice", pricingPhase.getFormattedPrice());
156+
serialized.put("priceCurrencyCode", pricingPhase.getPriceCurrencyCode());
157+
serialized.put("priceAmountMicros", pricingPhase.getPriceAmountMicros());
158+
serialized.put("billingCycleCount", pricingPhase.getBillingCycleCount());
159+
serialized.put("billingPeriod", pricingPhase.getBillingPeriod());
160+
serialized.put("recurrenceMode", pricingPhase.getRecurrenceMode());
161+
162+
return serialized;
163+
}
164+
59165
static HashMap<String, Object> fromPurchase(Purchase purchase) {
60166
HashMap<String, Object> info = new HashMap<>();
61-
// TODO(stuartmorgan): Migrate this code. See TODO on MethodCallHandlerImpl.querySkuDetailsAsync.
62-
@SuppressWarnings("deprecation")
63-
List<String> skus = purchase.getSkus();
167+
List<String> products = purchase.getProducts();
64168
info.put("orderId", purchase.getOrderId());
65169
info.put("packageName", purchase.getPackageName());
66170
info.put("purchaseTime", purchase.getPurchaseTime());
67171
info.put("purchaseToken", purchase.getPurchaseToken());
68172
info.put("signature", purchase.getSignature());
69-
info.put("skus", skus);
173+
info.put("products", products);
70174
info.put("isAutoRenewing", purchase.isAutoRenewing());
71175
info.put("originalJson", purchase.getOriginalJson());
72176
info.put("developerPayload", purchase.getDeveloperPayload());
@@ -84,13 +188,11 @@ static HashMap<String, Object> fromPurchase(Purchase purchase) {
84188
static HashMap<String, Object> fromPurchaseHistoryRecord(
85189
PurchaseHistoryRecord purchaseHistoryRecord) {
86190
HashMap<String, Object> info = new HashMap<>();
87-
// TODO(stuartmorgan): Migrate this code. See TODO on MethodCallHandlerImpl.querySkuDetailsAsync.
88-
@SuppressWarnings("deprecation")
89-
List<String> skus = purchaseHistoryRecord.getSkus();
191+
List<String> products = purchaseHistoryRecord.getProducts();
90192
info.put("purchaseTime", purchaseHistoryRecord.getPurchaseTime());
91193
info.put("purchaseToken", purchaseHistoryRecord.getPurchaseToken());
92194
info.put("signature", purchaseHistoryRecord.getSignature());
93-
info.put("skus", skus);
195+
info.put("products", products);
94196
info.put("developerPayload", purchaseHistoryRecord.getDeveloperPayload());
95197
info.put("originalJson", purchaseHistoryRecord.getOriginalJson());
96198
info.put("quantity", purchaseHistoryRecord.getQuantity());

0 commit comments

Comments
 (0)