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 966df5115e67..2f37f09478c8 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 9a5a74d0032b..663a4dfad46e 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 @@ android { } 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 b21ab6992608..6f4e4bbfd8ee 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 class MethodNames { static final String ON_PURCHASES_UPDATED = "PurchasesUpdatedListener#onPurchasesUpdated(int, List)"; 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 class MethodNames { 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 adad84b39e1d..ab12b2db8630 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 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { : 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 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { 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 @@ private void isReady(MethodChannel.Result result) { 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 skusList, final MethodChannel.Result result) { if (billingClientError(result)) { @@ -208,7 +220,6 @@ private void launchBillingFlow( if (billingClientError(result)) { return; } - SkuDetails skuDetails = cachedSkus.get(sku); if (skuDetails == null) { result.error( @@ -255,12 +266,15 @@ private void launchBillingFlow( 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 @@ public void onConsumeResponse(BillingResult billingResult, String outToken) { 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 purchasesList) { + final Map 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 @@ private void queryPurchaseHistoryAsync(String skuType, final MethodChannel.Resul } billingClient.queryPurchaseHistoryAsync( - skuType, + QueryPurchaseHistoryParams.newBuilder().setProductType(skuType).build(), new PurchaseHistoryResponseListener() { @Override public void onPurchaseHistoryResponse( @@ -316,6 +346,15 @@ public void onPurchaseHistoryResponse( }); } + private void getConnectionState(final MethodChannel.Result result) { + if (billingClientError(result)) { + return; + } + final Map 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 7546fe7db58d..5a0cf6ea3727 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 List> fromSkuDetailsList( static HashMap fromPurchase(Purchase purchase) { HashMap info = new HashMap<>(); + List 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 fromPurchase(Purchase purchase) { static HashMap fromPurchaseHistoryRecord( PurchaseHistoryRecord purchaseHistoryRecord) { HashMap info = new HashMap<>(); + List 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 @@ static List> fromPurchaseHistoryRecordList( return serialized; } - static HashMap fromPurchasesResult(PurchasesResult purchasesResult) { - HashMap info = new HashMap<>(); - info.put("responseCode", purchasesResult.getResponseCode()); - info.put("billingResult", fromBillingResult(purchasesResult.getBillingResult())); - info.put("purchasesList", fromPurchasesList(purchasesResult.getPurchasesList())); - return info; - } - static HashMap fromBillingResult(BillingResult billingResult) { HashMap 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 d676bf3436ee..e99ff46dd2cc 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 @@ public void launchBillingFlow_null_AccountId_do_not_crash() { 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 @@ public void launchBillingFlow_ok_null_OldSku() { 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 @@ public void launchBillingFlow_ok_oldSku() { 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 @@ public void launchBillingFlow_ok_AccountId() { 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 @@ public void launchBillingFlow_ok_Proration() { 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()); @@ -495,6 +483,43 @@ public void launchBillingFlow_ok_Proration_with_null_OldSku() { verify(result, never()).success(any()); } + @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 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 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 @@ -553,31 +578,6 @@ public void launchBillingFlow_oldSkuNotFound() { verify(result, never()).success(any()); } - @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 arguments = new HashMap<>(); - arguments.put("skuType", SkuType.INAPP); - methodChannelHandler.onMethodCall(new MethodCall(QUERY_PURCHASES, arguments), result); - - // Verify we pass the response to result - ArgumentCaptor> 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 @@ -585,7 +585,7 @@ public void queryPurchases_clientDisconnected() { HashMap 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 @@ public void queryPurchaseHistoryAsync() { // 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 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 2837dceea652..79852e7e8ca5 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; @@ -138,37 +135,6 @@ public void fromPurchasesList_null() { assertEquals(Collections.emptyList(), Translator.fromPurchasesList(null)); } - @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 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 serialized = Translator.fromPurchasesResult(result); - - assertEquals(BillingClient.BillingResponseCode.OK, serialized.get("responseCode")); - List> serializedPurchases = - (List>) serialized.get("purchasesList"); - assertEquals(expectedPurchases.size(), serializedPurchases.size()); - assertSerialized(expectedPurchases.get(0), serializedPurchases.get(0)); - assertSerialized(expectedPurchases.get(1), serializedPurchases.get(1)); - - Map billingResultMap = (Map) serialized.get("billingResult"); - assertEquals(billingResultMap.get("responseCode"), newBillingResult.getResponseCode()); - assertEquals(billingResultMap.get("debugMessage"), newBillingResult.getDebugMessage()); - } - @Test public void fromBillingResult() throws JSONException { BillingResult newBillingResult = @@ -232,7 +198,7 @@ private void assertSerialized(Purchase expected, Map serialized) 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 @@ private void assertSerialized(PurchaseHistoryRecord expected, Map map) => @@ -104,8 +105,13 @@ class PurchaseWrapper { 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: []) + final List skus; /// True for subscriptions that renew automatically. Does not apply to /// [SkuType.inapp] products. @@ -178,10 +184,11 @@ class PurchaseHistoryRecordWrapper { 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 map) => @@ -201,8 +208,14 @@ class PurchaseHistoryRecordWrapper { 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: []) + final List 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 5815a866c82d..7f6fd0f61d94 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 @@ PurchaseWrapper _$PurchaseWrapperFromJson(Map json) => PurchaseWrapper( 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() ?? + [] + : [], isAutoRenewing: json['isAutoRenewing'] as bool, originalJson: json['originalJson'] as String? ?? '', developerPayload: json['developerPayload'] as String?, @@ -28,7 +31,10 @@ PurchaseHistoryRecordWrapper _$PurchaseHistoryRecordWrapperFromJson(Map json) => 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() ?? + [] + : [], 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 b1e77666a0b5..db419b8e1aa4 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 @@ name: in_app_purchase_android 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 65c8bb213cc4..184d9331e6c1 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 @@ const PurchaseWrapper dummyPurchase = PurchaseWrapper( packageName: 'packageName', purchaseTime: 0, signature: 'signature', - sku: 'sku', + skus: ['sku'], purchaseToken: 'purchaseToken', isAutoRenewing: false, originalJson: '', @@ -27,7 +27,7 @@ const PurchaseWrapper dummyUnacknowledgedPurchase = PurchaseWrapper( packageName: 'packageName', purchaseTime: 0, signature: 'signature', - sku: 'sku', + skus: ['sku'], purchaseToken: 'purchaseToken', isAutoRenewing: false, originalJson: '', @@ -40,7 +40,7 @@ const PurchaseHistoryRecordWrapper dummyPurchaseHistoryRecord = PurchaseHistoryRecordWrapper( purchaseTime: 0, signature: 'signature', - sku: 'sku', + skus: ['sku'], purchaseToken: 'purchaseToken', originalJson: '', developerPayload: 'dummy payload', @@ -51,7 +51,7 @@ const PurchaseWrapper dummyOldPurchase = PurchaseWrapper( packageName: 'oldPackageName', purchaseTime: 0, signature: 'oldSignature', - sku: 'oldSku', + skus: ['oldSku'], purchaseToken: 'oldPurchaseToken', isAutoRenewing: false, originalJson: '', @@ -205,7 +205,7 @@ Map buildPurchaseMap(PurchaseWrapper original) { '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 @@ Map buildPurchaseHistoryRecordMap( return { '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 b19d631092e7..4f90dccf94f4 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 @@ void main() { 'purchasesList': [ { 'orderId': 'orderID1', - 'sku': skuDetails.sku, + 'skus': [skuDetails.sku], 'isAutoRenewing': false, 'packageName': 'package', 'purchaseTime': 1231231231, @@ -401,7 +401,7 @@ void main() { 'purchasesList': [ { 'orderId': 'orderID1', - 'sku': skuDetails.sku, + 'skus': [skuDetails.sku], 'isAutoRenewing': false, 'packageName': 'package', 'purchaseTime': 1231231231, @@ -515,7 +515,7 @@ void main() { 'purchasesList': [ { 'orderId': 'orderID1', - 'sku': skuDetails.sku, + 'skus': [skuDetails.sku], 'isAutoRenewing': false, 'packageName': 'package', 'purchaseTime': 1231231231, @@ -592,7 +592,7 @@ void main() { 'purchasesList': [ { 'orderId': 'orderID1', - 'sku': skuDetails.sku, + 'skus': [skuDetails.sku], 'isAutoRenewing': false, 'packageName': 'package', 'purchaseTime': 1231231231,