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 3afbeb605187..4724caf0e208 100644 --- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -1,6 +1,12 @@ -## NEXT +## 0.5.0 * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. +* Updates Google Play Billing Library from 7.1.1 to 8.0.0. +* **BREAKING CHANGES**: + * Removes `queryPurchaseHistory` and its wrapper `queryPurchaseHistoryAsync`. Use `queryPurchases` instead. +* Adds support for `subResponseCode` in `BillingResultWrapper`. +* Adds support for `oneTimePurchaseOfferDetailsList` in `ProductDetailsWrapper`. +* Adds support for `unfetchedProductList` in `ProductDetailsResponseWrapper` to handle product IDs that could not be fetched. ## 0.4.0+11 diff --git a/packages/in_app_purchase/in_app_purchase_android/README.md b/packages/in_app_purchase/in_app_purchase_android/README.md index be9f0f507259..45286f90b573 100644 --- a/packages/in_app_purchase/in_app_purchase_android/README.md +++ b/packages/in_app_purchase/in_app_purchase_android/README.md @@ -19,8 +19,9 @@ Using the Alternative billing only feature requires Google Play app configuratio [Google Play documentation for Alternative billing](https://developer.android.com/google/play/billing/alternative) -## Migrating to 0.3.0 -To migrate to version 0.3.0 from 0.2.x, have a look at the [migration guide](migration_guide.md). +## Migrating to 0.5.0 +To migrate to version 0.5.0 from 0.4.x or 0.3.0 from 0.2.x, have a look at the +[migration guide](migration_guide.md). [1]: https://pub.dev/packages/in_app_purchase [2]: https://flutter.dev/to/endorsed-federated-plugin diff --git a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle.kts b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle.kts index e70dbf7df8cf..94d243766b06 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle.kts +++ b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle.kts @@ -76,7 +76,7 @@ android { dependencies { implementation("androidx.annotation:annotation:1.9.1") - implementation("com.android.billingclient:billing:7.1.1") + implementation("com.android.billingclient:billing:8.0.0") testImplementation("junit:junit:4.13.2") testImplementation("org.json:json:20251224") testImplementation("org.mockito:mockito-core:5.23.0") 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 c934564366cb..3e665871b45a 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 @@ -8,8 +8,8 @@ import static io.flutter.plugins.inapppurchase.TranslatorKt.fromBillingConfig; import static io.flutter.plugins.inapppurchase.TranslatorKt.fromBillingResult; import static io.flutter.plugins.inapppurchase.TranslatorKt.fromProductDetailsList; -import static io.flutter.plugins.inapppurchase.TranslatorKt.fromPurchaseHistoryRecordList; import static io.flutter.plugins.inapppurchase.TranslatorKt.fromPurchasesList; +import static io.flutter.plugins.inapppurchase.TranslatorKt.fromUnfetchedProductList; import static io.flutter.plugins.inapppurchase.TranslatorKt.toBillingClientFeature; import static io.flutter.plugins.inapppurchase.TranslatorKt.toProductList; import static io.flutter.plugins.inapppurchase.TranslatorKt.toProductTypeString; @@ -33,7 +33,6 @@ import com.android.billingclient.api.GetBillingConfigParams; import com.android.billingclient.api.ProductDetails; import com.android.billingclient.api.QueryProductDetailsParams; -import com.android.billingclient.api.QueryPurchaseHistoryParams; import com.android.billingclient.api.QueryPurchasesParams; import java.util.ArrayList; import java.util.HashMap; @@ -225,11 +224,13 @@ public void queryProductDetailsAsync( QueryProductDetailsParams.newBuilder().setProductList(toProductList(products)).build(); billingClient.queryProductDetailsAsync( params, - (billingResult, productDetailsList) -> { - updateCachedProducts(productDetailsList); + (billingResult, productDetailsResult) -> { + updateCachedProducts(productDetailsResult.getProductDetailsList()); PlatformProductDetailsResponse response = new PlatformProductDetailsResponse( - fromBillingResult(billingResult), fromProductDetailsList(productDetailsList)); + fromBillingResult(billingResult), + fromProductDetailsList(productDetailsResult.getProductDetailsList()), + fromUnfetchedProductList(productDetailsResult.getUnfetchedProductList())); ResultCompat.success(response, callback); }); } catch (RuntimeException e) { @@ -402,33 +403,6 @@ public void queryPurchasesAsync( } } - @Override - @Deprecated - public void queryPurchaseHistoryAsync( - @NonNull PlatformProductType productType, - @NonNull Function1, Unit> callback) { - if (billingClient == null) { - ResultUtilsKt.completeWithError(callback, getNullBillingClientError()); - return; - } - - try { - billingClient.queryPurchaseHistoryAsync( - QueryPurchaseHistoryParams.newBuilder() - .setProductType(toProductTypeString(productType)) - .build(), - (billingResult, purchasesList) -> { - PlatformPurchaseHistoryResponse response = - new PlatformPurchaseHistoryResponse( - fromBillingResult(billingResult), fromPurchaseHistoryRecordList(purchasesList)); - ResultCompat.success(response, callback); - }); - } catch (RuntimeException e) { - ResultUtilsKt.completeWithError( - callback, new FlutterError("error", e.getMessage(), Log.getStackTraceString(e))); - } - } - @Override public void startConnection( long handle, diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/kotlin/io/flutter/plugins/inapppurchase/Messages.kt b/packages/in_app_purchase/in_app_purchase_android/android/src/main/kotlin/io/flutter/plugins/inapppurchase/Messages.kt index d206d17d3d19..9454d588eb28 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/kotlin/io/flutter/plugins/inapppurchase/Messages.kt +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/kotlin/io/flutter/plugins/inapppurchase/Messages.kt @@ -401,13 +401,15 @@ data class PlatformAccountIdentifiers( */ data class PlatformBillingResult( val responseCode: PlatformBillingResponse, - val debugMessage: String + val debugMessage: String, + val subResponseCode: Long ) { companion object { fun fromList(pigeonVar_list: List): PlatformBillingResult { val responseCode = pigeonVar_list[0] as PlatformBillingResponse val debugMessage = pigeonVar_list[1] as String - return PlatformBillingResult(responseCode, debugMessage) + val subResponseCode = pigeonVar_list[2] as Long + return PlatformBillingResult(responseCode, debugMessage, subResponseCode) } } @@ -415,6 +417,7 @@ data class PlatformBillingResult( return listOf( responseCode, debugMessage, + subResponseCode, ) } @@ -427,13 +430,15 @@ data class PlatformBillingResult( } val other = other as PlatformBillingResult return MessagesPigeonUtils.deepEquals(this.responseCode, other.responseCode) && - MessagesPigeonUtils.deepEquals(this.debugMessage, other.debugMessage) + MessagesPigeonUtils.deepEquals(this.debugMessage, other.debugMessage) && + MessagesPigeonUtils.deepEquals(this.subResponseCode, other.subResponseCode) } override fun hashCode(): Int { var result = javaClass.hashCode() result = 31 * result + MessagesPigeonUtils.deepHash(this.responseCode) result = 31 * result + MessagesPigeonUtils.deepHash(this.debugMessage) + result = 31 * result + MessagesPigeonUtils.deepHash(this.subResponseCode) return result } } @@ -500,6 +505,7 @@ data class PlatformProductDetails( val productType: PlatformProductType, val title: String, val oneTimePurchaseOfferDetails: PlatformOneTimePurchaseOfferDetails? = null, + val oneTimePurchaseOfferDetailsList: List? = null, val subscriptionOfferDetails: List? = null ) { companion object { @@ -510,7 +516,9 @@ data class PlatformProductDetails( val productType = pigeonVar_list[3] as PlatformProductType val title = pigeonVar_list[4] as String val oneTimePurchaseOfferDetails = pigeonVar_list[5] as PlatformOneTimePurchaseOfferDetails? - val subscriptionOfferDetails = pigeonVar_list[6] as List? + val oneTimePurchaseOfferDetailsList = + pigeonVar_list[6] as List? + val subscriptionOfferDetails = pigeonVar_list[7] as List? return PlatformProductDetails( description, name, @@ -518,6 +526,7 @@ data class PlatformProductDetails( productType, title, oneTimePurchaseOfferDetails, + oneTimePurchaseOfferDetailsList, subscriptionOfferDetails) } } @@ -530,6 +539,7 @@ data class PlatformProductDetails( productType, title, oneTimePurchaseOfferDetails, + oneTimePurchaseOfferDetailsList, subscriptionOfferDetails, ) } @@ -549,6 +559,8 @@ data class PlatformProductDetails( MessagesPigeonUtils.deepEquals(this.title, other.title) && MessagesPigeonUtils.deepEquals( this.oneTimePurchaseOfferDetails, other.oneTimePurchaseOfferDetails) && + MessagesPigeonUtils.deepEquals( + this.oneTimePurchaseOfferDetailsList, other.oneTimePurchaseOfferDetailsList) && MessagesPigeonUtils.deepEquals( this.subscriptionOfferDetails, other.subscriptionOfferDetails) } @@ -561,6 +573,7 @@ data class PlatformProductDetails( result = 31 * result + MessagesPigeonUtils.deepHash(this.productType) result = 31 * result + MessagesPigeonUtils.deepHash(this.title) result = 31 * result + MessagesPigeonUtils.deepHash(this.oneTimePurchaseOfferDetails) + result = 31 * result + MessagesPigeonUtils.deepHash(this.oneTimePurchaseOfferDetailsList) result = 31 * result + MessagesPigeonUtils.deepHash(this.subscriptionOfferDetails) return result } @@ -574,13 +587,15 @@ data class PlatformProductDetails( */ data class PlatformProductDetailsResponse( val billingResult: PlatformBillingResult, - val productDetails: List + val productDetails: List, + val unfetchedProductList: List ) { companion object { fun fromList(pigeonVar_list: List): PlatformProductDetailsResponse { val billingResult = pigeonVar_list[0] as PlatformBillingResult val productDetails = pigeonVar_list[1] as List - return PlatformProductDetailsResponse(billingResult, productDetails) + val unfetchedProductList = pigeonVar_list[2] as List + return PlatformProductDetailsResponse(billingResult, productDetails, unfetchedProductList) } } @@ -588,6 +603,7 @@ data class PlatformProductDetailsResponse( return listOf( billingResult, productDetails, + unfetchedProductList, ) } @@ -600,13 +616,15 @@ data class PlatformProductDetailsResponse( } val other = other as PlatformProductDetailsResponse return MessagesPigeonUtils.deepEquals(this.billingResult, other.billingResult) && - MessagesPigeonUtils.deepEquals(this.productDetails, other.productDetails) + MessagesPigeonUtils.deepEquals(this.productDetails, other.productDetails) && + MessagesPigeonUtils.deepEquals(this.unfetchedProductList, other.unfetchedProductList) } override fun hashCode(): Int { var result = javaClass.hashCode() result = 31 * result + MessagesPigeonUtils.deepHash(this.billingResult) result = 31 * result + MessagesPigeonUtils.deepHash(this.productDetails) + result = 31 * result + MessagesPigeonUtils.deepHash(this.unfetchedProductList) return result } } @@ -1433,6 +1451,44 @@ data class PlatformPendingPurchasesParams(val enablePrepaidPlans: Boolean) { } } +/** + * Pigeon version of Java + * [UnfetchedProduct](https://developer.android.com/reference/com/android/billingclient/api/QueryProductDetailsParams.Product). + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class PlatformUnfetchedProduct(val productId: String) { + companion object { + fun fromList(pigeonVar_list: List): PlatformUnfetchedProduct { + val productId = pigeonVar_list[0] as String + return PlatformUnfetchedProduct(productId) + } + } + + fun toList(): List { + return listOf( + productId, + ) + } + + override fun equals(other: Any?): Boolean { + if (other == null || other.javaClass != javaClass) { + return false + } + if (this === other) { + return true + } + val other = other as PlatformUnfetchedProduct + return MessagesPigeonUtils.deepEquals(this.productId, other.productId) + } + + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.productId) + return result + } +} + private open class MessagesPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return when (type) { @@ -1537,6 +1593,9 @@ private open class MessagesPigeonCodec : StandardMessageCodec() { PlatformPendingPurchasesParams.fromList(it) } } + 156.toByte() -> { + return (readValue(buffer) as? List)?.let { PlatformUnfetchedProduct.fromList(it) } + } else -> super.readValueOfType(type, buffer) } } @@ -1651,6 +1710,10 @@ private open class MessagesPigeonCodec : StandardMessageCodec() { stream.write(155) writeValue(stream, value.toList()) } + is PlatformUnfetchedProduct -> { + stream.write(156) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -1688,14 +1751,6 @@ interface InAppPurchaseApi { productType: PlatformProductType, callback: (Result) -> Unit ) - /** - * Wraps BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, - * PurchaseHistoryResponseListener). - */ - fun queryPurchaseHistoryAsync( - productType: PlatformProductType, - callback: (Result) -> Unit - ) /** * Wraps BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, * ProductDetailsResponseListener). @@ -1916,31 +1971,6 @@ interface InAppPurchaseApi { channel.setMessageHandler(null) } } - run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.queryPurchaseHistoryAsync$separatedMessageChannelSuffix", - codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val productTypeArg = args[0] as PlatformProductType - api.queryPurchaseHistoryAsync(productTypeArg) { - result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(MessagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(MessagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } run { val channel = BasicMessageChannel( diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/kotlin/io/flutter/plugins/inapppurchase/Translator.kt b/packages/in_app_purchase/in_app_purchase_android/android/src/main/kotlin/io/flutter/plugins/inapppurchase/Translator.kt index ae9ab0498948..c793dee0e3a8 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/kotlin/io/flutter/plugins/inapppurchase/Translator.kt +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/kotlin/io/flutter/plugins/inapppurchase/Translator.kt @@ -15,6 +15,7 @@ import com.android.billingclient.api.Purchase import com.android.billingclient.api.Purchase.PendingPurchaseUpdate import com.android.billingclient.api.PurchaseHistoryRecord import com.android.billingclient.api.QueryProductDetailsParams +import com.android.billingclient.api.UnfetchedProduct import com.android.billingclient.api.UserChoiceDetails fun fromProductDetail(detail: ProductDetails): PlatformProductDetails { @@ -26,6 +27,8 @@ fun fromProductDetail(detail: ProductDetails): PlatformProductDetails { title = detail.title, oneTimePurchaseOfferDetails = fromOneTimePurchaseOfferDetails(detail.oneTimePurchaseOfferDetails), + oneTimePurchaseOfferDetailsList = + fromOneTimePurchaseOfferDetailsList(detail.oneTimePurchaseOfferDetailsList), subscriptionOfferDetails = fromSubscriptionOfferDetailsList(detail.subscriptionOfferDetails)) } @@ -76,6 +79,13 @@ fun fromOneTimePurchaseOfferDetails( priceCurrencyCode = oneTimePurchaseOfferDetails.priceCurrencyCode) } +fun fromOneTimePurchaseOfferDetailsList( + oneTimePurchaseOfferDetailsList: List? +): List? { + // Using mapNotNull ensures the resulting list doesn't contain nulls + return oneTimePurchaseOfferDetailsList?.mapNotNull { fromOneTimePurchaseOfferDetails(it) } +} + fun fromSubscriptionOfferDetailsList( subscriptionOfferDetailsList: List? ): List? { @@ -206,9 +216,21 @@ fun fromPurchaseHistoryRecordList( return purchaseHistoryRecords?.map { fromPurchaseHistoryRecord(it) } ?: emptyList() } +fun fromUnfetchedProductList( + unfetchedProductList: List? +): List { + return unfetchedProductList?.map { fromUnfetchedProduct(it) } ?: emptyList() +} + +fun fromUnfetchedProduct(unfetchedProduct: UnfetchedProduct): PlatformUnfetchedProduct { + return PlatformUnfetchedProduct(productId = unfetchedProduct.productId) +} + fun fromBillingResult(billingResult: BillingResult): PlatformBillingResult { return PlatformBillingResult( - fromBillingResponseCode(billingResult.responseCode), billingResult.debugMessage) + fromBillingResponseCode(billingResult.responseCode), + billingResult.debugMessage, + billingResult.onPurchasesUpdatedSubResponseCode.toLong()) } fun fromBillingResponseCode(billingResponseCode: Int): PlatformBillingResponse = 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 80176aa78bcd..ce785007756b 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 @@ -49,10 +49,9 @@ import com.android.billingclient.api.ProductDetailsResponseListener; 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.QueryProductDetailsParams; -import com.android.billingclient.api.QueryPurchaseHistoryParams; +import com.android.billingclient.api.QueryProductDetailsResult; import com.android.billingclient.api.QueryPurchasesParams; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import java.lang.reflect.Constructor; @@ -86,7 +85,6 @@ public class MethodCallHandlerTest { TestResult platformBillingConfigResult = new TestResult<>(); TestResult platformBillingResult = new TestResult<>(); TestResult platformProductDetailsResult = new TestResult<>(); - TestResult platformPurchaseHistoryResult = new TestResult<>(); TestResult platformPurchasesResult = new TestResult<>(); @Mock Activity activity; @@ -541,12 +539,21 @@ public void queryProductDetailsAsync() { List productDetailsResponse = singletonList(buildProductDetails("foo")); BillingResult billingResult = buildBillingResult(); - listenerCaptor.getValue().onProductDetailsResponse(billingResult, productDetailsResponse); + QueryProductDetailsResult mockProductDetailsResult = mock(QueryProductDetailsResult.class); + when(mockProductDetailsResult.getProductDetailsList()).thenReturn(productDetailsResponse); + when(mockProductDetailsResult.getUnfetchedProductList()) + .thenReturn(java.util.Collections.emptyList()); + + listenerCaptor.getValue().onProductDetailsResponse(billingResult, mockProductDetailsResult); assertTrue(platformProductDetailsResult.called); PlatformProductDetailsResponse resultData = platformProductDetailsResult.result.getOrNull(); assertResultsMatch(resultData.getBillingResult(), billingResult); assertDetailListsMatch(productDetailsResponse, resultData.getProductDetails()); + assertTrue(resultData.getUnfetchedProductList().isEmpty()); + assertResultsMatch(resultData.getBillingResult(), billingResult); + assertDetailListsMatch(productDetailsResponse, resultData.getProductDetails()); + assertTrue(resultData.getUnfetchedProductList().isEmpty()); } @Test @@ -894,48 +901,6 @@ public void queryPurchases_returns_success() { assertTrue(purchasesResponse.getPurchases().isEmpty()); } - @Test - @SuppressWarnings(value = "deprecation") - public void queryPurchaseHistoryAsync() { - establishConnectedBillingClient(); - BillingResult billingResult = buildBillingResult(); - final String purchaseToken = "foo"; - List purchasesList = - singletonList(buildPurchaseHistoryRecord(purchaseToken)); - ArgumentCaptor listenerCaptor = - ArgumentCaptor.forClass(PurchaseHistoryResponseListener.class); - - methodChannelHandler.queryPurchaseHistoryAsync( - PlatformProductType.INAPP, platformPurchaseHistoryResult.asCallback()); - - verify(mockBillingClient) - .queryPurchaseHistoryAsync(any(QueryPurchaseHistoryParams.class), listenerCaptor.capture()); - listenerCaptor.getValue().onPurchaseHistoryResponse(billingResult, purchasesList); - - assertTrue(platformPurchaseHistoryResult.called); - PlatformPurchaseHistoryResponse result = platformPurchaseHistoryResult.result.getOrNull(); - assertResultsMatch(result.getBillingResult(), billingResult); - assertEquals(1, result.getPurchases().size()); - assertEquals(purchaseToken, result.getPurchases().get(0).getPurchaseToken()); - } - - @Test - @SuppressWarnings(value = "deprecation") - public void queryPurchaseHistoryAsync_clientDisconnected() { - methodChannelHandler.endConnection(); - - methodChannelHandler.queryPurchaseHistoryAsync( - PlatformProductType.INAPP, platformPurchaseHistoryResult.asCallback()); - - // Assert that the async call returns an error result. - assertTrue(platformPurchaseHistoryResult.called); - Throwable error = platformPurchaseHistoryResult.result.exceptionOrNull(); - assertTrue(error instanceof FlutterError); - FlutterError flutterError = (FlutterError) error; - assertEquals("UNAVAILABLE", flutterError.getCode()); - assertTrue(Objects.requireNonNull(flutterError.getMessage()).contains("BillingClient")); - } - @Test public void onPurchasesUpdatedListener() { PluginPurchaseListener listener = new PluginPurchaseListener(mockCallbackApi); @@ -1084,7 +1049,11 @@ private void queryForProducts(List productIdList) { productIdList.stream().map(this::buildProductDetails).collect(toList()); BillingResult billingResult = buildBillingResult(); - listenerCaptor.getValue().onProductDetailsResponse(billingResult, productDetailsResponse); + QueryProductDetailsResult mockProductDetailsResult = mock(QueryProductDetailsResult.class); + when(mockProductDetailsResult.getProductDetailsList()).thenReturn(productDetailsResponse); + when(mockProductDetailsResult.getUnfetchedProductList()) + .thenReturn(java.util.Collections.emptyList()); + listenerCaptor.getValue().onProductDetailsResponse(billingResult, mockProductDetailsResult); } private List buildProductList( 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 7f82b958400b..51dda74894ac 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 @@ -186,6 +186,9 @@ public void fromBillingResult_debugMessageNull() { assertEquals(PlatformBillingResponse.OK, platformResult.getResponseCode()); assertEquals(platformResult.getDebugMessage(), newBillingResult.getDebugMessage()); + assertEquals( + newBillingResult.getOnPurchasesUpdatedSubResponseCode(), + platformResult.getSubResponseCode()); } private void assertSerialized(ProductDetails expected, PlatformProductDetails serialized) { @@ -203,6 +206,21 @@ private void assertSerialized(ProductDetails expected, PlatformProductDetails se assertSerialized(expectedOneTimePurchaseOfferDetails, oneTimePurchaseOfferDetails); } + List expectedOfferList = + expected.getOneTimePurchaseOfferDetailsList(); + List serializedOfferList = + serialized.getOneTimePurchaseOfferDetailsList(); + + if (expectedOfferList == null) { + assertNull(serializedOfferList); + } else { + assertNotNull(serializedOfferList); + assertEquals(expectedOfferList.size(), serializedOfferList.size()); + for (int i = 0; i < expectedOfferList.size(); i++) { + assertSerialized(expectedOfferList.get(i), serializedOfferList.get(i)); + } + } + List expectedSubscriptionOfferDetailsList = expected.getSubscriptionOfferDetails(); List subscriptionOfferDetailsList = diff --git a/packages/in_app_purchase/in_app_purchase_android/example/integration_test/in_app_purchase_test.dart b/packages/in_app_purchase/in_app_purchase_android/example/integration_test/in_app_purchase_test.dart index b06a166dce11..e6be023edddb 100644 --- a/packages/in_app_purchase/in_app_purchase_android/example/integration_test/in_app_purchase_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/example/integration_test/in_app_purchase_test.dart @@ -111,18 +111,6 @@ void main() { } }); - testWidgets('BillingClient.queryPurchaseHistory', ( - WidgetTester tester, - ) async { - try { - // Intentional use of a deprecated method to make sure it still works. - // ignore: deprecated_member_use - await billingClient.queryPurchaseHistory(ProductType.inapp); - } on MissingPluginException { - fail('Method channel is not setup correctly'); - } - }); - testWidgets('BillingClient.queryPurchases', (WidgetTester tester) async { try { await billingClient.queryPurchases(ProductType.inapp); diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart index b3df0cf619b9..c49e67e9496a 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart @@ -249,29 +249,6 @@ class BillingClient { ); } - /// Fetches purchase history for the given [ProductType]. - /// - /// Unlike [queryPurchases], this makes a network request via Play and returns - /// the most recent purchase for each [ProductDetailsWrapper] of the given - /// [ProductType] even if the item is no longer owned. - /// - /// All purchase information should also be verified manually, with your - /// server if at all possible. See ["Verify a - /// purchase"](https://developer.android.com/google/play/billing/billing_library_overview#Verify). - /// - /// This wraps - /// [`BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchaseHistoryAsync(com.android.billingclient.api.QueryPurchaseHistoryParams,%20com.android.billingclient.api.PurchaseHistoryResponseListener)). - @Deprecated('Use queryPurchases') - Future queryPurchaseHistory( - ProductType productType, - ) async { - return purchaseHistoryResultFromPlatform( - await _hostApi.queryPurchaseHistoryAsync( - platformProductTypeFromWrapper(productType), - ), - ); - } - /// Consumes a given in-app product. /// /// Consuming can only be done on an item that's owned, and as a result of consumption, the user will no longer own it. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_response_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_response_wrapper.dart index 8f971b2cb405..22b83f37fe80 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_response_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_response_wrapper.dart @@ -17,12 +17,26 @@ const String kInvalidBillingResultErrorMessage = @immutable class BillingResultWrapper implements HasBillingResponse { /// Constructs the object with [responseCode] and [debugMessage]. - const BillingResultWrapper({required this.responseCode, this.debugMessage}); + const BillingResultWrapper({ + required this.responseCode, + this.subResponseCode = 0, + this.debugMessage, + }); /// Response code returned in the Play Billing API calls. @override final BillingResponse responseCode; + /// Sub-response code returned in the Play Billing API calls. + /// + /// Defaults to 0 which is returned when no other sub-response code is applicable. + /// + /// Possible values: + /// * `0`: `NO_APPLICABLE_SUB_RESPONSE_CODE` + /// * `1`: `PAYMENT_DECLINED_DUE_TO_INSUFFICIENT_FUNDS` + /// * `2`: `USER_INELIGIBLE` + final int subResponseCode; + /// Debug message returned in the Play Billing API calls. /// /// Defaults to `null`. @@ -39,9 +53,10 @@ class BillingResultWrapper implements HasBillingResponse { return other is BillingResultWrapper && other.responseCode == responseCode && + other.subResponseCode == subResponseCode && other.debugMessage == debugMessage; } @override - int get hashCode => Object.hash(responseCode, debugMessage); + int get hashCode => Object.hash(responseCode, subResponseCode, debugMessage); } diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/product_details_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/product_details_wrapper.dart index cf700162b237..ce8241113121 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/product_details_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/product_details_wrapper.dart @@ -17,6 +17,7 @@ class ProductDetailsWrapper { required this.description, required this.name, this.oneTimePurchaseOfferDetails, + this.oneTimePurchaseOfferDetailsList, required this.productId, required this.productType, this.subscriptionOfferDetails, @@ -38,6 +39,13 @@ class ProductDetailsWrapper { /// null for [ProductType.subs]. final OneTimePurchaseOfferDetailsWrapper? oneTimePurchaseOfferDetails; + /// The list of offer details for a one-time purchase product. + /// + /// [oneTimePurchaseOfferDetailsList] is only set for [ProductType.inapp]. + /// Returns null for [ProductType.subs]. + final List? + oneTimePurchaseOfferDetailsList; + /// The product's id. final String productId; @@ -66,6 +74,10 @@ class ProductDetailsWrapper { other.description == description && other.name == name && other.oneTimePurchaseOfferDetails == oneTimePurchaseOfferDetails && + listEquals( + other.oneTimePurchaseOfferDetailsList, + oneTimePurchaseOfferDetailsList, + ) && other.productId == productId && other.productType == productType && listEquals(other.subscriptionOfferDetails, subscriptionOfferDetails) && @@ -78,6 +90,7 @@ class ProductDetailsWrapper { description.hashCode, name.hashCode, oneTimePurchaseOfferDetails.hashCode, + oneTimePurchaseOfferDetailsList.hashCode, productId.hashCode, productType.hashCode, subscriptionOfferDetails.hashCode, @@ -95,6 +108,7 @@ class ProductDetailsResponseWrapper implements HasBillingResponse { const ProductDetailsResponseWrapper({ required this.billingResult, required this.productDetailsList, + this.unfetchedProductList = const [], }); /// The final result of the [BillingClient.queryProductDetails] call. @@ -103,6 +117,9 @@ class ProductDetailsResponseWrapper implements HasBillingResponse { /// A list of [ProductDetailsWrapper] matching the query to [BillingClient.queryProductDetails]. final List productDetailsList; + /// A list of [UnfetchedProductWrapper] that could not be fetched by [BillingClient.queryProductDetails]. + final List unfetchedProductList; + @override BillingResponse get responseCode => billingResult.responseCode; @@ -114,11 +131,37 @@ class ProductDetailsResponseWrapper implements HasBillingResponse { return other is ProductDetailsResponseWrapper && other.billingResult == billingResult && - other.productDetailsList == productDetailsList; + listEquals(other.productDetailsList, productDetailsList) && + listEquals(other.unfetchedProductList, unfetchedProductList); + } + + @override + int get hashCode => + Object.hash(billingResult, productDetailsList, unfetchedProductList); +} + +/// Dart wrapper around [`com.android.billingclient.api.QueryProductDetailsParams.Product`](https://developer.android.com/reference/com/android/billingclient/api/QueryProductDetailsParams.Product). +/// +/// Contains the details of a product that could not be fetched by the Google Play Billing Library. +@immutable +class UnfetchedProductWrapper { + /// Creates an [UnfetchedProductWrapper]. + const UnfetchedProductWrapper({required this.productId}); + + /// The product ID that could not be fetched. + final String productId; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + + return other is UnfetchedProductWrapper && other.productId == productId; } @override - int get hashCode => Object.hash(billingResult, productDetailsList); + int get hashCode => productId.hashCode; } /// Recurrence mode of the pricing phase. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart index 6e100a89f40d..21889b84ebb0 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/messages.g.dart @@ -268,14 +268,17 @@ class PlatformBillingResult { PlatformBillingResult({ required this.responseCode, required this.debugMessage, + this.subResponseCode = 0, }); PlatformBillingResponse responseCode; String debugMessage; + int subResponseCode; + List _toList() { - return [responseCode, debugMessage]; + return [responseCode, debugMessage, subResponseCode]; } Object encode() { @@ -287,6 +290,7 @@ class PlatformBillingResult { return PlatformBillingResult( responseCode: result[0]! as PlatformBillingResponse, debugMessage: result[1]! as String, + subResponseCode: result[2]! as int, ); } @@ -300,7 +304,8 @@ class PlatformBillingResult { return true; } return _deepEquals(responseCode, other.responseCode) && - _deepEquals(debugMessage, other.debugMessage); + _deepEquals(debugMessage, other.debugMessage) && + _deepEquals(subResponseCode, other.subResponseCode); } @override @@ -368,6 +373,7 @@ class PlatformProductDetails { required this.productType, required this.title, this.oneTimePurchaseOfferDetails, + this.oneTimePurchaseOfferDetailsList, this.subscriptionOfferDetails, }); @@ -383,6 +389,8 @@ class PlatformProductDetails { PlatformOneTimePurchaseOfferDetails? oneTimePurchaseOfferDetails; + List? oneTimePurchaseOfferDetailsList; + List? subscriptionOfferDetails; List _toList() { @@ -393,6 +401,7 @@ class PlatformProductDetails { productType, title, oneTimePurchaseOfferDetails, + oneTimePurchaseOfferDetailsList, subscriptionOfferDetails, ]; } @@ -411,7 +420,9 @@ class PlatformProductDetails { title: result[4]! as String, oneTimePurchaseOfferDetails: result[5] as PlatformOneTimePurchaseOfferDetails?, - subscriptionOfferDetails: (result[6] as List?) + oneTimePurchaseOfferDetailsList: (result[6] as List?) + ?.cast(), + subscriptionOfferDetails: (result[7] as List?) ?.cast(), ); } @@ -434,6 +445,10 @@ class PlatformProductDetails { oneTimePurchaseOfferDetails, other.oneTimePurchaseOfferDetails, ) && + _deepEquals( + oneTimePurchaseOfferDetailsList, + other.oneTimePurchaseOfferDetailsList, + ) && _deepEquals(subscriptionOfferDetails, other.subscriptionOfferDetails); } @@ -448,14 +463,17 @@ class PlatformProductDetailsResponse { PlatformProductDetailsResponse({ required this.billingResult, required this.productDetails, + required this.unfetchedProductList, }); PlatformBillingResult billingResult; List productDetails; + List unfetchedProductList; + List _toList() { - return [billingResult, productDetails]; + return [billingResult, productDetails, unfetchedProductList]; } Object encode() { @@ -468,6 +486,8 @@ class PlatformProductDetailsResponse { billingResult: result[0]! as PlatformBillingResult, productDetails: (result[1]! as List) .cast(), + unfetchedProductList: (result[2]! as List) + .cast(), ); } @@ -482,7 +502,8 @@ class PlatformProductDetailsResponse { return true; } return _deepEquals(billingResult, other.billingResult) && - _deepEquals(productDetails, other.productDetails); + _deepEquals(productDetails, other.productDetails) && + _deepEquals(unfetchedProductList, other.unfetchedProductList); } @override @@ -1361,6 +1382,43 @@ class PlatformPendingPurchasesParams { int get hashCode => _deepHash([runtimeType, ..._toList()]); } +/// Pigeon version of Java [UnfetchedProduct](https://developer.android.com/reference/com/android/billingclient/api/QueryProductDetailsParams.Product). +class PlatformUnfetchedProduct { + PlatformUnfetchedProduct({required this.productId}); + + String productId; + + List _toList() { + return [productId]; + } + + Object encode() { + return _toList(); + } + + static PlatformUnfetchedProduct decode(Object result) { + result as List; + return PlatformUnfetchedProduct(productId: result[0]! as String); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformUnfetchedProduct || + other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(productId, other.productId); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -1450,6 +1508,9 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is PlatformPendingPurchasesParams) { buffer.putUint8(155); writeValue(buffer, value.encode()); + } else if (value is PlatformUnfetchedProduct) { + buffer.putUint8(156); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -1523,6 +1584,8 @@ class _PigeonCodec extends StandardMessageCodec { return PlatformInstallmentPlanDetails.decode(readValue(buffer)!); case 155: return PlatformPendingPurchasesParams.decode(readValue(buffer)!); + case 156: + return PlatformUnfetchedProduct.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -1725,30 +1788,6 @@ class InAppPurchaseApi { return pigeonVar_replyValue! as PlatformPurchasesResponse; } - /// Wraps BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener). - Future queryPurchaseHistoryAsync( - PlatformProductType productType, - ) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.queryPurchaseHistoryAsync$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [productType], - ); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as PlatformPurchaseHistoryResponse; - } - /// Wraps BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener). Future queryProductDetailsAsync( List products, diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart index c3586df9c6a7..64aa8fac4102 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/pigeon_converters.dart @@ -25,6 +25,7 @@ PlatformBillingChoiceMode platformBillingChoiceMode(BillingChoiceMode mode) { BillingResultWrapper resultWrapperFromPlatform(PlatformBillingResult result) { return BillingResultWrapper( responseCode: billingResponseFromPlatform(result.responseCode), + subResponseCode: result.subResponseCode, debugMessage: result.debugMessage, ); } @@ -38,6 +39,9 @@ ProductDetailsResponseWrapper productDetailsResponseWrapperFromPlatform( productDetailsList: response.productDetails .map(productDetailsWrapperFromPlatform) .toList(), + unfetchedProductList: response.unfetchedProductList + .map(unfetchedProductWrapperFromPlatform) + .toList(), ); } @@ -54,12 +58,23 @@ ProductDetailsWrapper productDetailsWrapperFromPlatform( oneTimePurchaseOfferDetails: oneTimePurchaseOfferDetailsWrapperFromPlatform( product.oneTimePurchaseOfferDetails, ), + oneTimePurchaseOfferDetailsList: product.oneTimePurchaseOfferDetailsList + ?.map(oneTimePurchaseOfferDetailsWrapperFromPlatform) + .whereType() + .toList(), subscriptionOfferDetails: product.subscriptionOfferDetails ?.map(subscriptionOfferDetailsWrapperFromPlatform) .toList(), ); } +/// Creates a [UnfetchedProductWrapper] from the Pigeon equivalent. +UnfetchedProductWrapper unfetchedProductWrapperFromPlatform( + PlatformUnfetchedProduct product, +) { + return UnfetchedProductWrapper(productId: product.productId); +} + /// Creates a [OneTimePurchaseOfferDetailsWrapper] from the Pigeon equivalent. OneTimePurchaseOfferDetailsWrapper? oneTimePurchaseOfferDetailsWrapperFromPlatform( diff --git a/packages/in_app_purchase/in_app_purchase_android/migration_guide.md b/packages/in_app_purchase/in_app_purchase_android/migration_guide.md index cb81042b31b5..38dda3f94538 100644 --- a/packages/in_app_purchase/in_app_purchase_android/migration_guide.md +++ b/packages/in_app_purchase/in_app_purchase_android/migration_guide.md @@ -1,4 +1,28 @@ +# Migration Guide from 0.4.x to 0.5.0 + +Version 0.5.0 updates to use Google Play Billing Library 8.0.0. This update includes breaking changes unrelated to the Dart API surface, but specific methods in `BillingClientWrapper` have been removed to align with the native library. + +## Removal of `queryPurchaseHistory` + +The `queryPurchaseHistory` method in `BillingClientWrapper` has been removed because the underlying +native method `queryPurchaseHistoryAsync` was removed in Google Play Billing Library 7.0.0. + +Instead, use `queryPurchases` (which calls `queryPurchasesAsync` natively) to fetch active purchases. +This is now the recommended way to check for existing purchases. + +`queryPurchaseHistory` previously allowed checking for canceled, refunded, or voided purchases. +With its removal, this information is no longer available through the Billing Client. +The [Google Play Developer API](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.products/get) +can be used to verify the state of past purchases. + +### Enhancements + +* `BillingResultWrapper` and `PlatformBillingResult` now include a `subResponseCode` field. +* `ProductDetailsResponseWrapper` and `PlatformProductDetailsResponse` now include an +`unfetchedProductList` to identify products that could not be retrieved. +* `ProductDetailsWrapper` now supports `oneTimePurchaseOfferDetailsList` for products with multiple buy options. + # Migration Guide from 0.2.x to 0.3.0 Starting November 2023, Android Billing Client V4 is no longer supported, diff --git a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart index 7aa6c9e84ceb..4ca51b4a6130 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart +++ b/packages/in_app_purchase/in_app_purchase_android/pigeons/messages.dart @@ -37,9 +37,11 @@ class PlatformBillingResult { PlatformBillingResult({ required this.responseCode, required this.debugMessage, + this.subResponseCode = 0, }); final PlatformBillingResponse responseCode; final String debugMessage; + final int subResponseCode; } /// Pigeon version of Java BillingClient.BillingResponseCode. @@ -81,6 +83,7 @@ class PlatformProductDetails { required this.productType, required this.title, required this.oneTimePurchaseOfferDetails, + required this.oneTimePurchaseOfferDetailsList, required this.subscriptionOfferDetails, }); @@ -90,6 +93,8 @@ class PlatformProductDetails { final PlatformProductType productType; final String title; final PlatformOneTimePurchaseOfferDetails? oneTimePurchaseOfferDetails; + final List? + oneTimePurchaseOfferDetailsList; final List? subscriptionOfferDetails; } @@ -99,10 +104,12 @@ class PlatformProductDetailsResponse { PlatformProductDetailsResponse({ required this.billingResult, required this.productDetails, + required this.unfetchedProductList, }); final PlatformBillingResult billingResult; final List productDetails; + final List unfetchedProductList; } /// Pigeon version of AlternativeBillingOnlyReportingDetailsWrapper, which @@ -344,6 +351,13 @@ class PlatformPendingPurchasesParams { final bool enablePrepaidPlans; } +/// Pigeon version of Java [UnfetchedProduct](https://developer.android.com/reference/com/android/billingclient/api/QueryProductDetailsParams.Product). +class PlatformUnfetchedProduct { + PlatformUnfetchedProduct({required this.productId}); + + final String productId; +} + /// Pigeon version of Java BillingClient.ProductType. enum PlatformProductType { inapp, subs } @@ -416,12 +430,6 @@ abstract class InAppPurchaseApi { PlatformProductType productType, ); - /// Wraps BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener). - @async - PlatformPurchaseHistoryResponse queryPurchaseHistoryAsync( - PlatformProductType productType, - ); - /// Wraps BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener). @async PlatformProductDetailsResponse queryProductDetailsAsync( 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 5fa109480283..cc769821eaea 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,8 @@ 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/packages/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.4.0+11 + +version: 0.5.0 environment: sdk: ^3.10.0 diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index 2959ea704712..b9099a02179c 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -195,6 +195,7 @@ void main() { debugMessage: debugMessage, ), productDetails: [], + unfetchedProductList: [], ), ); @@ -228,6 +229,7 @@ void main() { productDetails: [ convertToPigeonProductDetails(dummyOneTimeProductDetails), ], + unfetchedProductList: [], ), ); @@ -248,6 +250,35 @@ void main() { expect(response.billingResult, equals(billingResult)); expect(response.productDetailsList, contains(dummyOneTimeProductDetails)); }); + + test('returns unfetchedProductList', () async { + const debugMessage = 'dummy message'; + when(mockApi.queryProductDetailsAsync(any)).thenAnswer( + (_) async => PlatformProductDetailsResponse( + billingResult: PlatformBillingResult( + responseCode: PlatformBillingResponse.ok, + debugMessage: debugMessage, + ), + productDetails: [], + unfetchedProductList: [ + PlatformUnfetchedProduct(productId: 'unfetched'), + ], + ), + ); + + final ProductDetailsResponseWrapper response = await billingClient + .queryProductDetails( + productList: [ + const ProductWrapper( + productId: 'unfetched', + productType: ProductType.inapp, + ), + ], + ); + + expect(response.unfetchedProductList, hasLength(1)); + expect(response.unfetchedProductList[0].productId, 'unfetched'); + }); }); group('launchBillingFlow', () { @@ -256,6 +287,7 @@ void main() { const BillingResponse responseCode = BillingResponse.ok; const expectedBillingResult = BillingResultWrapper( responseCode: responseCode, + subResponseCode: 123, debugMessage: debugMessage, ); when( @@ -525,32 +557,6 @@ void main() { }); }); - group('queryPurchaseHistory', () { - test('handles empty purchases', () async { - const BillingResponse expectedCode = BillingResponse.userCanceled; - const debugMessage = 'dummy message'; - const expectedBillingResult = BillingResultWrapper( - responseCode: expectedCode, - debugMessage: debugMessage, - ); - when(mockApi.queryPurchaseHistoryAsync(any)).thenAnswer( - (_) async => PlatformPurchaseHistoryResponse( - billingResult: PlatformBillingResult( - responseCode: PlatformBillingResponse.userCanceled, - debugMessage: debugMessage, - ), - purchases: [], - ), - ); - - final PurchasesHistoryResult response = await billingClient - .queryPurchaseHistory(ProductType.inapp); - - expect(response.billingResult, equals(expectedBillingResult)); - expect(response.purchaseHistoryRecordList, isEmpty); - }); - }); - group('consume purchases', () { test('consume purchase async success', () async { const token = 'dummy token'; @@ -684,6 +690,15 @@ void main() { expect(result, expected); }); }); + + test('UnfetchedProductWrapper equality', () { + const product1 = UnfetchedProductWrapper(productId: 'id'); + const product2 = UnfetchedProductWrapper(productId: 'id'); + const product3 = UnfetchedProductWrapper(productId: 'other'); + + expect(product1, product2); + expect(product1, isNot(product3)); + }); } PlatformBillingConfigResponse platformBillingConfigFromWrapper( diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/product_details_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/product_details_wrapper_test.dart index a2277eaff969..8023514abb74 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/product_details_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/product_details_wrapper_test.dart @@ -51,6 +51,13 @@ void main() { priceAmountMicros: 10, priceCurrencyCode: 'priceCurrencyCode', ), + oneTimePurchaseOfferDetailsList: [ + OneTimePurchaseOfferDetailsWrapper( + formattedPrice: 'formattedPrice', + priceAmountMicros: 10, + priceCurrencyCode: 'priceCurrencyCode', + ), + ], subscriptionOfferDetails: [ SubscriptionOfferDetailsWrapper( basePlanId: 'basePlanId', @@ -84,6 +91,13 @@ void main() { priceAmountMicros: 10, priceCurrencyCode: 'priceCurrencyCode', ), + oneTimePurchaseOfferDetailsList: [ + OneTimePurchaseOfferDetailsWrapper( + formattedPrice: 'formattedPrice', + priceAmountMicros: 10, + priceCurrencyCode: 'priceCurrencyCode', + ), + ], subscriptionOfferDetails: [ SubscriptionOfferDetailsWrapper( basePlanId: 'basePlanId', @@ -115,10 +129,12 @@ void main() { test('operator == of BillingResultWrapper works fine', () { const firstBillingResultInstance = BillingResultWrapper( responseCode: BillingResponse.ok, + subResponseCode: 123, debugMessage: 'debugMessage', ); const secondBillingResultInstance = BillingResultWrapper( responseCode: BillingResponse.ok, + subResponseCode: 123, debugMessage: 'debugMessage', ); expect(firstBillingResultInstance == secondBillingResultInstance, isTrue); 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 c29edc9e861e..e3b87aa20e3e 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 @@ -47,6 +47,10 @@ const ProductDetailsWrapper dummySubscriptionProductDetails = ], ); +final PlatformUnfetchedProduct dummyUnfetchedProduct = PlatformUnfetchedProduct( + productId: 'unfetched', +); + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -149,6 +153,7 @@ void main() { debugMessage: debugMessage, ), productDetails: [], + unfetchedProductList: [], ), ); @@ -169,6 +174,9 @@ void main() { productDetails: [ convertToPigeonProductDetails(dummyOneTimeProductDetails), ], + unfetchedProductList: [ + dummyUnfetchedProduct, + ], ), ); // Since queryProductDetails makes 2 platform method calls (one for each ProductType), the result will contain 2 dummyWrapper instead @@ -202,6 +210,7 @@ void main() { productDetails: [ convertToPigeonProductDetails(dummyOneTimeProductDetails), ], + unfetchedProductList: [], ), ); // Since queryProductDetails makes 2 platform method calls (one for each ProductType), the result will contain 2 dummyWrapper instead diff --git a/packages/in_app_purchase/in_app_purchase_android/test/test_conversion_utils.dart b/packages/in_app_purchase/in_app_purchase_android/test/test_conversion_utils.dart index b80ab3574580..2493939abc19 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/test_conversion_utils.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/test_conversion_utils.dart @@ -15,6 +15,7 @@ PlatformBillingResult convertToPigeonResult(BillingResultWrapper targetResult) { return PlatformBillingResult( responseCode: billingResponseFromWrapper(targetResult.responseCode), debugMessage: targetResult.debugMessage!, + subResponseCode: targetResult.subResponseCode, ); }