Skip to content

Commit

Permalink
[in_app_purchase] Fully migrate to BillingClient V5 (flutter#3752)
Browse files Browse the repository at this point in the history
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 [flutter#110909](flutter#110909)
* Fixes [flutter#107370](flutter#107370)
* Fixes [flutter#114265](flutter#114265)
  • Loading branch information
JeroenWeener authored May 17, 2023
1 parent 2b38236 commit dc5ff42
Show file tree
Hide file tree
Showing 37 changed files with 2,076 additions and 1,482 deletions.
4 changes: 4 additions & 0 deletions packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.3.0
* **BREAKING CHANGE**: Removes `launchPriceChangeConfirmationFlow` from `InAppPurchaseAndroidPlatform`. Price changes are now [handled by Google Play](https://developer.android.com/google/play/billing/subscriptions#price-change).
* Returns both base plans and offers when `queryProductDetailsAsync` is called.

## 0.2.5+5

* Updates gradle, AGP and fixes some lint errors.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,21 @@ static final class MethodNames {
"BillingClient#startConnection(BillingClientStateListener)";
static final String END_CONNECTION = "BillingClient#endConnection()";
static final String ON_DISCONNECT = "BillingClientStateListener#onBillingServiceDisconnected()";
static final String QUERY_SKU_DETAILS =
"BillingClient#querySkuDetailsAsync(SkuDetailsParams, SkuDetailsResponseListener)";
static final String QUERY_PRODUCT_DETAILS =
"BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener)";
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 = "BillingClient#queryPurchases(String)";
static final String QUERY_PURCHASES_ASYNC = "BillingClient#queryPurchasesAsync(String)";
"PurchasesUpdatedListener#onPurchasesUpdated(BillingResult, List<Purchase>)";
static final String QUERY_PURCHASES_ASYNC =
"BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)";
static final String QUERY_PURCHASE_HISTORY_ASYNC =
"BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)";
"BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener)";
static final String CONSUME_PURCHASE_ASYNC =
"BillingClient#consumeAsync(String, ConsumeResponseListener)";
"BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener)";
static final String ACKNOWLEDGE_PURCHASE =
"BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)";
"BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)";
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,69 +4,173 @@

package io.flutter.plugins.inapppurchase;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.billingclient.api.AccountIdentifiers;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ProductDetails;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchaseHistoryRecord;
import com.android.billingclient.api.QueryProductDetailsParams;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Currency;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/** Handles serialization of {@link com.android.billingclient.api.BillingClient} related objects. */
/**
* Handles serialization and deserialization of {@link com.android.billingclient.api.BillingClient}
* related objects.
*/
/*package*/ class Translator {
// TODO(stuartmorgan): Migrate this code. See TODO on MethodCallHandlerImpl.querySkuDetailsAsync.
@SuppressWarnings("deprecation")
static HashMap<String, Object> fromSkuDetail(com.android.billingclient.api.SkuDetails detail) {
static HashMap<String, Object> fromProductDetail(ProductDetails detail) {
HashMap<String, Object> info = new HashMap<>();
info.put("title", detail.getTitle());
info.put("description", detail.getDescription());
info.put("freeTrialPeriod", detail.getFreeTrialPeriod());
info.put("introductoryPrice", detail.getIntroductoryPrice());
info.put("introductoryPriceAmountMicros", detail.getIntroductoryPriceAmountMicros());
info.put("introductoryPriceCycles", detail.getIntroductoryPriceCycles());
info.put("introductoryPricePeriod", detail.getIntroductoryPricePeriod());
info.put("price", detail.getPrice());
info.put("priceAmountMicros", detail.getPriceAmountMicros());
info.put("priceCurrencyCode", detail.getPriceCurrencyCode());
info.put("priceCurrencySymbol", currencySymbolFromCode(detail.getPriceCurrencyCode()));
info.put("sku", detail.getSku());
info.put("type", detail.getType());
info.put("subscriptionPeriod", detail.getSubscriptionPeriod());
info.put("originalPrice", detail.getOriginalPrice());
info.put("originalPriceAmountMicros", detail.getOriginalPriceAmountMicros());
info.put("productId", detail.getProductId());
info.put("productType", detail.getProductType());
info.put("name", detail.getName());

@Nullable
ProductDetails.OneTimePurchaseOfferDetails oneTimePurchaseOfferDetails =
detail.getOneTimePurchaseOfferDetails();
if (oneTimePurchaseOfferDetails != null) {
info.put(
"oneTimePurchaseOfferDetails",
fromOneTimePurchaseOfferDetails(oneTimePurchaseOfferDetails));
}

@Nullable
List<ProductDetails.SubscriptionOfferDetails> subscriptionOfferDetailsList =
detail.getSubscriptionOfferDetails();
if (subscriptionOfferDetailsList != null) {
info.put(
"subscriptionOfferDetails",
fromSubscriptionOfferDetailsList(subscriptionOfferDetailsList));
}

return info;
}

// TODO(stuartmorgan): Migrate this code. See TODO on MethodCallHandlerImpl.querySkuDetailsAsync.
@SuppressWarnings("deprecation")
static List<HashMap<String, Object>> fromSkuDetailsList(
@Nullable List<com.android.billingclient.api.SkuDetails> skuDetailsList) {
if (skuDetailsList == null) {
static List<QueryProductDetailsParams.Product> toProductList(List<Object> serialized) {
List<QueryProductDetailsParams.Product> products = new ArrayList<>();
for (Object productSerialized : serialized) {
@SuppressWarnings(value = "unchecked")
Map<String, Object> productMap = (Map<String, Object>) productSerialized;
products.add(toProduct(productMap));
}
return products;
}

static QueryProductDetailsParams.Product toProduct(Map<String, Object> serialized) {
String productId = (String) serialized.get("productId");
String productType = (String) serialized.get("productType");
return QueryProductDetailsParams.Product.newBuilder()
.setProductId(productId)
.setProductType(productType)
.build();
}

static List<HashMap<String, Object>> fromProductDetailsList(
@Nullable List<ProductDetails> productDetailsList) {
if (productDetailsList == null) {
return Collections.emptyList();
}

ArrayList<HashMap<String, Object>> output = new ArrayList<>();
for (com.android.billingclient.api.SkuDetails detail : skuDetailsList) {
output.add(fromSkuDetail(detail));
for (ProductDetails detail : productDetailsList) {
output.add(fromProductDetail(detail));
}
return output;
}

static HashMap<String, Object> fromOneTimePurchaseOfferDetails(
@Nullable ProductDetails.OneTimePurchaseOfferDetails oneTimePurchaseOfferDetails) {
HashMap<String, Object> serialized = new HashMap<>();
if (oneTimePurchaseOfferDetails == null) {
return serialized;
}

serialized.put("priceAmountMicros", oneTimePurchaseOfferDetails.getPriceAmountMicros());
serialized.put("priceCurrencyCode", oneTimePurchaseOfferDetails.getPriceCurrencyCode());
serialized.put("formattedPrice", oneTimePurchaseOfferDetails.getFormattedPrice());

return serialized;
}

static List<HashMap<String, Object>> fromSubscriptionOfferDetailsList(
@Nullable List<ProductDetails.SubscriptionOfferDetails> subscriptionOfferDetailsList) {
if (subscriptionOfferDetailsList == null) {
return Collections.emptyList();
}

ArrayList<HashMap<String, Object>> serialized = new ArrayList<>();

for (ProductDetails.SubscriptionOfferDetails subscriptionOfferDetails :
subscriptionOfferDetailsList) {
serialized.add(fromSubscriptionOfferDetails(subscriptionOfferDetails));
}

return serialized;
}

static HashMap<String, Object> fromSubscriptionOfferDetails(
@Nullable ProductDetails.SubscriptionOfferDetails subscriptionOfferDetails) {
HashMap<String, Object> serialized = new HashMap<>();
if (subscriptionOfferDetails == null) {
return serialized;
}

serialized.put("offerId", subscriptionOfferDetails.getOfferId());
serialized.put("basePlanId", subscriptionOfferDetails.getBasePlanId());
serialized.put("offerTags", subscriptionOfferDetails.getOfferTags());
serialized.put("offerIdToken", subscriptionOfferDetails.getOfferToken());

ProductDetails.PricingPhases pricingPhases = subscriptionOfferDetails.getPricingPhases();
serialized.put("pricingPhases", fromPricingPhases(pricingPhases));

return serialized;
}

static List<HashMap<String, Object>> fromPricingPhases(
@NonNull ProductDetails.PricingPhases pricingPhases) {
ArrayList<HashMap<String, Object>> serialized = new ArrayList<>();

for (ProductDetails.PricingPhase pricingPhase : pricingPhases.getPricingPhaseList()) {
serialized.add(fromPricingPhase(pricingPhase));
}
return serialized;
}

static HashMap<String, Object> fromPricingPhase(
@Nullable ProductDetails.PricingPhase pricingPhase) {
HashMap<String, Object> serialized = new HashMap<>();

if (pricingPhase == null) {
return serialized;
}

serialized.put("formattedPrice", pricingPhase.getFormattedPrice());
serialized.put("priceCurrencyCode", pricingPhase.getPriceCurrencyCode());
serialized.put("priceAmountMicros", pricingPhase.getPriceAmountMicros());
serialized.put("billingCycleCount", pricingPhase.getBillingCycleCount());
serialized.put("billingPeriod", pricingPhase.getBillingPeriod());
serialized.put("recurrenceMode", pricingPhase.getRecurrenceMode());

return serialized;
}

static HashMap<String, Object> fromPurchase(Purchase purchase) {
HashMap<String, Object> info = new HashMap<>();
// TODO(stuartmorgan): Migrate this code. See TODO on MethodCallHandlerImpl.querySkuDetailsAsync.
@SuppressWarnings("deprecation")
List<String> skus = purchase.getSkus();
List<String> products = purchase.getProducts();
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("skus", skus);
info.put("products", products);
info.put("isAutoRenewing", purchase.isAutoRenewing());
info.put("originalJson", purchase.getOriginalJson());
info.put("developerPayload", purchase.getDeveloperPayload());
Expand All @@ -84,13 +188,11 @@ static HashMap<String, Object> fromPurchase(Purchase purchase) {
static HashMap<String, Object> fromPurchaseHistoryRecord(
PurchaseHistoryRecord purchaseHistoryRecord) {
HashMap<String, Object> info = new HashMap<>();
// TODO(stuartmorgan): Migrate this code. See TODO on MethodCallHandlerImpl.querySkuDetailsAsync.
@SuppressWarnings("deprecation")
List<String> skus = purchaseHistoryRecord.getSkus();
List<String> products = purchaseHistoryRecord.getProducts();
info.put("purchaseTime", purchaseHistoryRecord.getPurchaseTime());
info.put("purchaseToken", purchaseHistoryRecord.getPurchaseToken());
info.put("signature", purchaseHistoryRecord.getSignature());
info.put("skus", skus);
info.put("products", products);
info.put("developerPayload", purchaseHistoryRecord.getDeveloperPayload());
info.put("originalJson", purchaseHistoryRecord.getOriginalJson());
info.put("quantity", purchaseHistoryRecord.getQuantity());
Expand Down
Loading

0 comments on commit dc5ff42

Please sign in to comment.