[in_app_purchase] Migrate android to Billing to 5.0.0 (#5405)

diff --git a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md
index 966df51..2f37f09 100644
--- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md
+++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 0.2.3
+
+* Upgrades Google Play Billing Library to 5.0
+* Migrates APIs to support breaking changes in new Google Play Billing API
+* `PurchaseWrapper` and `PurchaseHistoryRecordWrapper` now handles `skus` a list of sku strings. `sku` is deprecated.
+
 ## 0.2.2+8
 
 * Ignores deprecation warnings for upcoming styleFrom button API changes.
diff --git a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle
index 9a5a74d..663a4df 100644
--- a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle
+++ b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle
@@ -52,11 +52,11 @@
 }
 
 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'
 }
diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java
index b21ab69..6f4e4bb 100644
--- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java
+++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java
@@ -39,6 +39,7 @@
     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 =
@@ -48,6 +49,7 @@
     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() {};
   }
diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java
index adad84b..ab12b2d 100644
--- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java
+++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java
@@ -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;
@@ -25,8 +25,12 @@
 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.QueryPurchaseHistoryParams;
+import com.android.billingclient.api.QueryPurchasesParams;
 import com.android.billingclient.api.SkuDetails;
 import com.android.billingclient.api.SkuDetailsParams;
 import com.android.billingclient.api.SkuDetailsResponseListener;
@@ -131,10 +135,14 @@
                 : 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:
+        Log.e("flutter", (String) call.argument("skuType"));
         queryPurchaseHistoryAsync((String) call.argument("skuType"), result);
         break;
       case InAppPurchasePlugin.MethodNames.CONSUME_PURCHASE_ASYNC:
@@ -149,6 +157,9 @@
       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();
     }
@@ -174,6 +185,7 @@
     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)) {
@@ -208,7 +220,6 @@
     if (billingClientError(result)) {
       return;
     }
-
     SkuDetails skuDetails = cachedSkus.get(sku);
     if (skuDetails == null) {
       result.error(
@@ -255,12 +266,15 @@
     if (obfuscatedProfileId != null && !obfuscatedProfileId.isEmpty()) {
       paramsBuilder.setObfuscatedProfileId(obfuscatedProfileId);
     }
-    if (oldSku != null && !oldSku.isEmpty()) {
-      paramsBuilder.setOldSku(oldSku, purchaseToken);
+    BillingFlowParams.SubscriptionUpdateParams.Builder subscriptionUpdateParamsBuilder =
+        BillingFlowParams.SubscriptionUpdateParams.newBuilder();
+    if (oldSku != null && !oldSku.isEmpty() && purchaseToken != null) {
+      subscriptionUpdateParamsBuilder.setOldPurchaseToken(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.setReplaceProrationMode(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())));
@@ -286,14 +300,30 @@
     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) {
@@ -302,7 +332,7 @@
     }
 
     billingClient.queryPurchaseHistoryAsync(
-        skuType,
+        QueryPurchaseHistoryParams.newBuilder().setProductType(skuType).build(),
         new PurchaseHistoryResponseListener() {
           @Override
           public void onPurchaseHistoryResponse(
@@ -316,6 +346,15 @@
         });
   }
 
+  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);
diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java
index 7546fe7..5a0cf6e 100644
--- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java
+++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java
@@ -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;
@@ -56,17 +55,19 @@
 
   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());
@@ -78,12 +79,14 @@
   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;
   }
 
@@ -112,14 +115,6 @@
     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());
diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java
index d676bf3..e99ff46 100644
--- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java
+++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java
@@ -13,21 +13,19 @@
 import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW;
 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.singletonList;
 import static java.util.Collections.unmodifiableList;
 import static java.util.stream.Collectors.toList;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.contains;
@@ -56,9 +54,9 @@
 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.QueryPurchaseHistoryParams;
 import com.android.billingclient.api.SkuDetails;
 import com.android.billingclient.api.SkuDetailsParams;
 import com.android.billingclient.api.SkuDetailsResponseListener;
@@ -294,7 +292,6 @@
         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());
@@ -327,8 +324,6 @@
         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));
@@ -380,8 +375,6 @@
         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());
@@ -413,7 +406,6 @@
         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());
@@ -451,10 +443,6 @@
         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());
@@ -496,6 +484,43 @@
   }
 
   @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
     MethodCall disconnectCall = new MethodCall(END_CONNECTION, null);
@@ -554,38 +579,13 @@
   }
 
   @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());
@@ -613,7 +613,7 @@
 
     // Verify we pass the data to result
     verify(mockBillingClient)
-        .queryPurchaseHistoryAsync(eq(SkuType.INAPP), listenerCaptor.capture());
+        .queryPurchaseHistoryAsync(any(QueryPurchaseHistoryParams.class), listenerCaptor.capture());
     listenerCaptor.getValue().onPurchaseHistoryResponse(billingResult, purchasesList);
     verify(result).success(resultCaptor.capture());
     HashMap<String, Object> resultData = resultCaptor.getValue();
diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java
index 2837dce..79852e7 100644
--- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java
+++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java
@@ -9,15 +9,12 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
 
 import androidx.annotation.NonNull;
 import com.android.billingclient.api.AccountIdentifiers;
 import com.android.billingclient.api.BillingClient;
 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.Arrays;
@@ -139,37 +136,6 @@
   }
 
   @Test
-  public void fromPurchasesResult() throws JSONException {
-    PurchasesResult result = mock(PurchasesResult.class);
-    final String purchase2Json =
-        "{\"orderId\":\"foo2\",\"packageName\":\"bar\",\"productId\":\"consumable\",\"purchaseTime\":11111111,\"purchaseState\":0,\"purchaseToken\":\"baz\",\"developerPayload\":\"dummy payload\",\"isAcknowledged\":\"true\"}";
-    final String signature = "signature";
-    final List<Purchase> expectedPurchases =
-        Arrays.asList(
-            new Purchase(PURCHASE_EXAMPLE_JSON, signature), new Purchase(purchase2Json, signature));
-    when(result.getPurchasesList()).thenReturn(expectedPurchases);
-    when(result.getResponseCode()).thenReturn(BillingClient.BillingResponseCode.OK);
-    BillingResult newBillingResult =
-        BillingResult.newBuilder()
-            .setDebugMessage("dummy debug message")
-            .setResponseCode(BillingClient.BillingResponseCode.OK)
-            .build();
-    when(result.getBillingResult()).thenReturn(newBillingResult);
-    final HashMap<String, Object> serialized = Translator.fromPurchasesResult(result);
-
-    assertEquals(BillingClient.BillingResponseCode.OK, serialized.get("responseCode"));
-    List<Map<String, Object>> serializedPurchases =
-        (List<Map<String, Object>>) serialized.get("purchasesList");
-    assertEquals(expectedPurchases.size(), serializedPurchases.size());
-    assertSerialized(expectedPurchases.get(0), serializedPurchases.get(0));
-    assertSerialized(expectedPurchases.get(1), serializedPurchases.get(1));
-
-    Map<String, Object> billingResultMap = (Map<String, Object>) serialized.get("billingResult");
-    assertEquals(billingResultMap.get("responseCode"), newBillingResult.getResponseCode());
-    assertEquals(billingResultMap.get("debugMessage"), newBillingResult.getDebugMessage());
-  }
-
-  @Test
   public void fromBillingResult() throws JSONException {
     BillingResult newBillingResult =
         BillingResult.newBuilder()
@@ -232,7 +198,7 @@
     assertEquals(expected.getPurchaseToken(), serialized.get("purchaseToken"));
     assertEquals(expected.getSignature(), serialized.get("signature"));
     assertEquals(expected.getOriginalJson(), serialized.get("originalJson"));
-    assertEquals(expected.getSku(), serialized.get("sku"));
+    assertEquals(expected.getSkus(), serialized.get("skus"));
     assertEquals(expected.getDeveloperPayload(), serialized.get("developerPayload"));
     assertEquals(expected.isAcknowledged(), serialized.get("isAcknowledged"));
     assertEquals(expected.getPurchaseState(), serialized.get("purchaseState"));
@@ -251,7 +217,7 @@
     assertEquals(expected.getPurchaseToken(), serialized.get("purchaseToken"));
     assertEquals(expected.getSignature(), serialized.get("signature"));
     assertEquals(expected.getOriginalJson(), serialized.get("originalJson"));
-    assertEquals(expected.getSku(), serialized.get("sku"));
+    assertEquals(expected.getSkus(), serialized.get("skus"));
     assertEquals(expected.getDeveloperPayload(), serialized.get("developerPayload"));
   }
 }
diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/app/build.gradle b/packages/in_app_purchase/in_app_purchase_android/example/android/app/build.gradle
index b7dc185..498355a 100644
--- a/packages/in_app_purchase/in_app_purchase_android/example/android/app/build.gradle
+++ b/packages/in_app_purchase/in_app_purchase_android/example/android/app/build.gradle
@@ -72,7 +72,7 @@
     defaultConfig {
         applicationId project.APP_ID
         minSdkVersion 16
-        targetSdkVersion 29
+        targetSdkVersion 30
         versionCode project.VERSION_CODE
         versionName project.VERSION_NAME
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -106,7 +106,7 @@
 }
 
 dependencies {
-    implementation 'com.android.billingclient:billing:3.0.2'
+    implementation 'com.android.billingclient:billing:5.0.0'
     testImplementation 'junit:junit:4.13.2'
     testImplementation 'org.mockito:mockito-core:3.6.0'
     testImplementation 'org.json:json:20180813'
diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart
index efaf079..af1aaa5 100644
--- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart
+++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart
@@ -33,7 +33,8 @@
     required this.purchaseTime,
     required this.purchaseToken,
     required this.signature,
-    required this.sku,
+    @Deprecated('Use skus instead') String? sku,
+    required this.skus,
     required this.isAutoRenewing,
     required this.originalJson,
     this.developerPayload,
@@ -41,7 +42,7 @@
     required this.purchaseState,
     this.obfuscatedAccountId,
     this.obfuscatedProfileId,
-  });
+  }) : _sku = sku;
 
   /// Factory for creating a [PurchaseWrapper] from a [Map] with the purchase details.
   factory PurchaseWrapper.fromJson(Map<String, dynamic> map) =>
@@ -104,8 +105,13 @@
   final String signature;
 
   /// The product ID of this purchase.
-  @JsonKey(defaultValue: '')
-  final String sku;
+  @Deprecated('Use skus instead')
+  String get sku => _sku ?? (skus.isNotEmpty ? skus.first : '');
+  final String? _sku;
+
+  /// The product IDs of this purchase.
+  @JsonKey(defaultValue: <String>[])
+  final List<String> skus;
 
   /// True for subscriptions that renew automatically. Does not apply to
   /// [SkuType.inapp] products.
@@ -178,10 +184,11 @@
     required this.purchaseTime,
     required this.purchaseToken,
     required this.signature,
-    required this.sku,
+    @Deprecated('Use skus instead') String? sku,
+    required this.skus,
     required this.originalJson,
     required this.developerPayload,
-  });
+  }) : _sku = sku;
 
   /// Factory for creating a [PurchaseHistoryRecordWrapper] from a [Map] with the record details.
   factory PurchaseHistoryRecordWrapper.fromJson(Map<String, dynamic> map) =>
@@ -201,8 +208,14 @@
   final String signature;
 
   /// The product ID of this purchase.
-  @JsonKey(defaultValue: '')
-  final String sku;
+  @Deprecated('Use skus instead')
+  String get sku => _sku ?? (skus.isNotEmpty ? skus.first : '');
+
+  final String? _sku;
+
+  /// The product ID of this purchase.
+  @JsonKey(defaultValue: <String>[])
+  final List<String> skus;
 
   /// Details about this purchase, in JSON.
   ///
diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart
index 5815a86..7f6fd0f 100644
--- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart
+++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart
@@ -12,7 +12,10 @@
       purchaseTime: json['purchaseTime'] as int? ?? 0,
       purchaseToken: json['purchaseToken'] as String? ?? '',
       signature: json['signature'] as String? ?? '',
-      sku: json['sku'] as String? ?? '',
+      skus: json['skus'] != null
+          ? (json['skus'] as List)?.map((item) => item as String)?.toList() ??
+              <String>[]
+          : <String>[],
       isAutoRenewing: json['isAutoRenewing'] as bool,
       originalJson: json['originalJson'] as String? ?? '',
       developerPayload: json['developerPayload'] as String?,
@@ -28,7 +31,10 @@
       purchaseTime: json['purchaseTime'] as int? ?? 0,
       purchaseToken: json['purchaseToken'] as String? ?? '',
       signature: json['signature'] as String? ?? '',
-      sku: json['sku'] as String? ?? '',
+      skus: json['skus'] != null
+          ? (json['skus'] as List)?.map((item) => item as String)?.toList() ??
+              <String>[]
+          : <String>[],
       originalJson: json['originalJson'] as String? ?? '',
       developerPayload: json['developerPayload'] as String?,
     );
diff --git a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml
index b1e7766..db419b8 100644
--- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml
+++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml
@@ -2,7 +2,7 @@
 description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs.
 repository: https://github.com/flutter/plugins/tree/main/packages/in_app_purchase/in_app_purchase_android
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22
-version: 0.2.2+8
+version: 0.2.3
 
 environment:
   sdk: ">=2.14.0 <3.0.0"
diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart
index 65c8bb2..184d933 100644
--- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart
+++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart
@@ -11,7 +11,7 @@
   packageName: 'packageName',
   purchaseTime: 0,
   signature: 'signature',
-  sku: 'sku',
+  skus: <String>['sku'],
   purchaseToken: 'purchaseToken',
   isAutoRenewing: false,
   originalJson: '',
@@ -27,7 +27,7 @@
   packageName: 'packageName',
   purchaseTime: 0,
   signature: 'signature',
-  sku: 'sku',
+  skus: <String>['sku'],
   purchaseToken: 'purchaseToken',
   isAutoRenewing: false,
   originalJson: '',
@@ -40,7 +40,7 @@
     PurchaseHistoryRecordWrapper(
   purchaseTime: 0,
   signature: 'signature',
-  sku: 'sku',
+  skus: <String>['sku'],
   purchaseToken: 'purchaseToken',
   originalJson: '',
   developerPayload: 'dummy payload',
@@ -51,7 +51,7 @@
   packageName: 'oldPackageName',
   purchaseTime: 0,
   signature: 'oldSignature',
-  sku: 'oldSku',
+  skus: <String>['oldSku'],
   purchaseToken: 'oldPurchaseToken',
   isAutoRenewing: false,
   originalJson: '',
@@ -205,7 +205,7 @@
     'packageName': original.packageName,
     'purchaseTime': original.purchaseTime,
     'signature': original.signature,
-    'sku': original.sku,
+    'skus': original.skus,
     'purchaseToken': original.purchaseToken,
     'isAutoRenewing': original.isAutoRenewing,
     'originalJson': original.originalJson,
@@ -223,7 +223,7 @@
   return <String, dynamic>{
     'purchaseTime': original.purchaseTime,
     'signature': original.signature,
-    'sku': original.sku,
+    'skus': original.skus,
     'purchaseToken': original.purchaseToken,
     'originalJson': original.originalJson,
     'developerPayload': original.developerPayload,
diff --git a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart
index b19d631..4f90dcc 100644
--- a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart
+++ b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart
@@ -299,7 +299,7 @@
               'purchasesList': <dynamic>[
                 <dynamic, dynamic>{
                   'orderId': 'orderID1',
-                  'sku': skuDetails.sku,
+                  'skus': <String>[skuDetails.sku],
                   'isAutoRenewing': false,
                   'packageName': 'package',
                   'purchaseTime': 1231231231,
@@ -401,7 +401,7 @@
               'purchasesList': <dynamic>[
                 <dynamic, dynamic>{
                   'orderId': 'orderID1',
-                  'sku': skuDetails.sku,
+                  'skus': <String>[skuDetails.sku],
                   'isAutoRenewing': false,
                   'packageName': 'package',
                   'purchaseTime': 1231231231,
@@ -515,7 +515,7 @@
               'purchasesList': <dynamic>[
                 <dynamic, dynamic>{
                   'orderId': 'orderID1',
-                  'sku': skuDetails.sku,
+                  'skus': <String>[skuDetails.sku],
                   'isAutoRenewing': false,
                   'packageName': 'package',
                   'purchaseTime': 1231231231,
@@ -592,7 +592,7 @@
               'purchasesList': <dynamic>[
                 <dynamic, dynamic>{
                   'orderId': 'orderID1',
-                  'sku': skuDetails.sku,
+                  'skus': <String>[skuDetails.sku],
                   'isAutoRenewing': false,
                   'packageName': 'package',
                   'purchaseTime': 1231231231,