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 0974ea26b1..32b558b1af 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,7 @@ +## 0.3.0+8 + +* Adds a [guide for migrating](migration_guide.md) to [0.3.0](#0.3.0). + ## 0.3.0+7 * Bumps org.mockito:mockito-core from 4.7.0 to 5.3.1. 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 f00cf164e5..d49315b41a 100644 --- a/packages/in_app_purchase/in_app_purchase_android/README.md +++ b/packages/in_app_purchase/in_app_purchase_android/README.md @@ -11,6 +11,9 @@ so you do not need to add it to your `pubspec.yaml`. However, if you `import` this package to use any of its APIs directly, you should [add it to your `pubspec.yaml` as usual][3]. +## 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). + ## Contributing This plugin uses diff --git a/packages/in_app_purchase/in_app_purchase_android/example/build.excerpt.yaml b/packages/in_app_purchase/in_app_purchase_android/example/build.excerpt.yaml new file mode 100644 index 0000000000..e317efa11c --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/example/build.excerpt.yaml @@ -0,0 +1,15 @@ +targets: + $default: + sources: + include: + - lib/** + # Some default includes that aren't really used here but will prevent + # false-negative warnings: + - $package$ + - lib/$lib$ + exclude: + - '**/.*/**' + - '**/build/**' + builders: + code_excerpter|code_excerpter: + enabled: true diff --git a/packages/in_app_purchase/in_app_purchase_android/example/lib/migration_guide_examples.dart b/packages/in_app_purchase/in_app_purchase_android/example/lib/migration_guide_examples.dart new file mode 100644 index 0000000000..05eda2f3bd --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/example/lib/migration_guide_examples.dart @@ -0,0 +1,62 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: unused_local_variable + +import 'package:in_app_purchase_android/billing_client_wrappers.dart'; +import 'package:in_app_purchase_android/in_app_purchase_android.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +// #docregion one-time-purchase-price +/// Handles the one time purchase price of a product. +void handleOneTimePurchasePrice(ProductDetails productDetails) { + if (productDetails is GooglePlayProductDetails) { + final ProductDetailsWrapper product = productDetails.productDetails; + if (product.productType == ProductType.inapp) { + // Unwrapping is safe because the product is a one time purchase. + final OneTimePurchaseOfferDetailsWrapper offer = + product.oneTimePurchaseOfferDetails!; + final String price = offer.formattedPrice; + } + } +} +// #enddocregion one-time-purchase-price + +// #docregion subscription-free-trial +/// Handles the free trial period of a subscription. +void handleFreeTrialPeriod(ProductDetails productDetails) { + if (productDetails is GooglePlayProductDetails) { + final ProductDetailsWrapper product = productDetails.productDetails; + if (product.productType == ProductType.subs) { + // Unwrapping is safe because the product is a subscription. + final SubscriptionOfferDetailsWrapper offer = + product.subscriptionOfferDetails![productDetails.subscriptionIndex!]; + final List pricingPhases = offer.pricingPhases; + if (pricingPhases.first.priceAmountMicros == 0) { + // Free trial period logic. + } + } + } +} +// #enddocregion subscription-free-trial + +// #docregion subscription-introductory-price +/// Handles the introductory price period of a subscription. +void handleIntroductoryPricePeriod(ProductDetails productDetails) { + if (productDetails is GooglePlayProductDetails) { + final ProductDetailsWrapper product = productDetails.productDetails; + if (product.productType == ProductType.subs) { + // Unwrapping is safe because the product is a subscription. + final SubscriptionOfferDetailsWrapper offer = + product.subscriptionOfferDetails![productDetails.subscriptionIndex!]; + final List pricingPhases = offer.pricingPhases; + if (pricingPhases.length >= 2 && + pricingPhases.first.priceAmountMicros < + pricingPhases[1].priceAmountMicros) { + // Introductory pricing period logic. + } + } + } +} +// #enddocregion subscription-introductory-price diff --git a/packages/in_app_purchase/in_app_purchase_android/example/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/example/pubspec.yaml index a2b59a3265..5938153ae8 100644 --- a/packages/in_app_purchase/in_app_purchase_android/example/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/example/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: shared_preferences: ^2.0.0 dev_dependencies: + build_runner: ^2.4.5 flutter_driver: sdk: flutter flutter_test: 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 new file mode 100644 index 0000000000..48a2e47c10 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/migration_guide.md @@ -0,0 +1,157 @@ + +# Migration Guide from 0.2.x to 0.3.0 + +Starting November 2023, Android Billing Client V4 is no longer supported, +requiring app developers using the plugin to migrate to 0.3.0. +Version 0.3.0 introduces breaking changes to the API as a result of changes in +the underlying Google Play Billing Library. +For context around the changes see the [recent changes to subscriptions in Play Console][1] +and the [Google Play Billing Library migration guide][2]. + +## SKUs become Products + +SKUs have been replaced with products. For example, the `SkuDetailsWrapper` +class has been replaced by `ProductDetailsWrapper`, and the `SkuType` enum has +been replaced by `ProductType`. + +Products are used to represent both in-app purchases (also referred to as +one-time purchases) and subscriptions. There are a few changes to the data model +that are important to note. + +Previously, an SKU of `SkuType.subs` would contain a `freeTrialPeriod`, +`introductoryPrice` and some related properties. In the new model, this has been +replaced by a more generic approach. +A subscription product can have multiple pricing phases, each with its own +price, subscription period etc. This allows for more flexibility in the pricing +model. + +In the next section we will look at how to migrate code that uses the old model +to the new model. Instead of listing all possible use cases, we will focus on +three: getting the price of a one time purchase, and handling both free trials +and introductory prices for subscriptions. Other use cases can be migrated in a +similar way. + +### Use case: getting the price of a one time purchase + +Below code shows how to get the price of a one time purchase before and after +the migration. Other properties can be obtained in a similar way. + +Code before migration: + +```dart +SkuDetailsWrapper sku; + +if (sku.type == SkuType.inapp) { + String price = sku.price; +} +``` + +Code after migration: + + +```dart +/// Handles the one time purchase price of a product. +void handleOneTimePurchasePrice(ProductDetails productDetails) { + if (productDetails is GooglePlayProductDetails) { + final ProductDetailsWrapper product = productDetails.productDetails; + if (product.productType == ProductType.inapp) { + // Unwrapping is safe because the product is a one time purchase. + final OneTimePurchaseOfferDetailsWrapper offer = + product.oneTimePurchaseOfferDetails!; + final String price = offer.formattedPrice; + } + } +} +``` + +### Use case: free trials + +Below code shows how to handle free trials for subscriptions before and after +the migration. As subscriptions can contain multiple offers, each containing +multiple price phases, the logic is more involved. + +Code before migration: + +```dart +SkuDetailsWrapper sku; + +if (sku.type == SkuType.subs) { + if (sku.freeTrialPeriod.isNotEmpty) { + // Free trial period logic. + } +} +``` + +Code after migration: + + +```dart +/// Handles the free trial period of a subscription. +void handleFreeTrialPeriod(ProductDetails productDetails) { + if (productDetails is GooglePlayProductDetails) { + final ProductDetailsWrapper product = productDetails.productDetails; + if (product.productType == ProductType.subs) { + // Unwrapping is safe because the product is a subscription. + final SubscriptionOfferDetailsWrapper offer = + product.subscriptionOfferDetails![productDetails.subscriptionIndex!]; + final List pricingPhases = offer.pricingPhases; + if (pricingPhases.first.priceAmountMicros == 0) { + // Free trial period logic. + } + } + } +} +``` + +### Use case: introductory prices + +Below code shows how to handle introductory prices for subscriptions before and +after the migration. As subscriptions can contain multiple offers, each +containing multiple price phases, the logic is more involved. + +Code before migration: + +```dart +SkuDetailsWrapper sku; + +if (sku.type == SkuType.subs) { + if (sku.introductoryPriceAmountMicros != 0) { + // Introductory price period logic. + } +} +``` + +Code after migration: + + +```dart +/// Handles the introductory price period of a subscription. +void handleIntroductoryPricePeriod(ProductDetails productDetails) { + if (productDetails is GooglePlayProductDetails) { + final ProductDetailsWrapper product = productDetails.productDetails; + if (product.productType == ProductType.subs) { + // Unwrapping is safe because the product is a subscription. + final SubscriptionOfferDetailsWrapper offer = + product.subscriptionOfferDetails![productDetails.subscriptionIndex!]; + final List pricingPhases = offer.pricingPhases; + if (pricingPhases.length >= 2 && + pricingPhases.first.priceAmountMicros < + pricingPhases[1].priceAmountMicros) { + // Introductory pricing period logic. + } + } + } +} +``` + +## Removal of `launchPriceChangeConfirmationFlow` + +The method `launchPriceChangeConfirmationFlow` has been removed. Price changes +are no longer handled by the application but are instead handled by the Google +Play Store automatically. See [subscription price changes][3] for more +information. + + +[1]: https://support.google.com/googleplay/android-developer/answer/12124625 +[2]: https://developer.android.com/google/play/billing/migrate-gpblv6#5-or-6 +[3]: https://developer.android.com/google/play/billing/subscriptions#price-change 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 158f8ea8e2..a68abcfc30 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/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.3.0+7 +version: 0.3.0+8 environment: sdk: ">=2.18.0 <4.0.0"