diff --git a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md index d4a2e5ab131d..c91973ca355f 100644 --- a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md @@ -1,204 +1,206 @@ ## NEXT -* Updates minimum supported SDK version to Flutter 3.19/Dart 3.3. +- Updates minimum supported SDK version to Flutter 3.19/Dart 3.3. +- Updates `in_app_purchase_android` to 0.4.0. ## 3.2.0 -* Adds `countryCode` API. -* Updates minimum supported SDK version to Flutter 3.13/Dart 3.1. -* Updates support matrix in README to indicate that iOS 11 is no longer supported. -* Clients on versions of Flutter that still support iOS 11 can continue to use this +- Adds `countryCode` API. +- Updates minimum supported SDK version to Flutter 3.13/Dart 3.1. +- Updates support matrix in README to indicate that iOS 11 is no longer supported. +- Clients on versions of Flutter that still support iOS 11 can continue to use this package with iOS 11, but will not receive any further updates to the iOS implementation. ## 3.1.13 -* Updates minimum required plugin_platform_interface version to 2.1.7. +- Updates minimum required plugin_platform_interface version to 2.1.7. ## 3.1.12 -* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. -* Fixes new lint warnings. +- Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. +- Fixes new lint warnings. ## 3.1.11 -* Updates documentation reference of `finishPurchase` to `completePurchase`. +- Updates documentation reference of `finishPurchase` to `completePurchase`. ## 3.1.10 -* Updates example code for current versions of Flutter. +- Updates example code for current versions of Flutter. ## 3.1.9 -* Adds pub topics to package metadata. -* Updates minimum supported SDK version to Flutter 3.7/Dart 2.19. +- Adds pub topics to package metadata. +- Updates minimum supported SDK version to Flutter 3.7/Dart 2.19. ## 3.1.8 -* Updates documentation on handling subscription price changes to match Android's billing client v5. +- Updates documentation on handling subscription price changes to match Android's billing client v5. ## 3.1.7 -* Fixes unawaited_futures violations. +- Fixes unawaited_futures violations. ## 3.1.6 -* Bumps minimum in_app_purchase_android version to 0.3.0. -* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. -* Aligns Dart and Flutter SDK constraints. +- Bumps minimum in_app_purchase_android version to 0.3.0. +- Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. +- Aligns Dart and Flutter SDK constraints. ## 3.1.5 -* Updates links for the merge of flutter/plugins into flutter/packages. +- Updates links for the merge of flutter/plugins into flutter/packages. ## 3.1.4 -* Updates iOS minimum version in README. +- Updates iOS minimum version in README. ## 3.1.3 -* Ignores a lint in the example app for backwards compatibility. +- Ignores a lint in the example app for backwards compatibility. ## 3.1.2 -* Updates example code for `use_build_context_synchronously` lint. -* Updates minimum Flutter version to 3.0. +- Updates example code for `use_build_context_synchronously` lint. +- Updates minimum Flutter version to 3.0. ## 3.1.1 -* Adds screenshots to pubspec.yaml. +- Adds screenshots to pubspec.yaml. ## 3.1.0 -* Adds macOS as a supported platform. +- Adds macOS as a supported platform. ## 3.0.8 -* Updates minimum Flutter version to 2.10. -* Bumps minimum in_app_purchase_android to 0.2.3. +- Updates minimum Flutter version to 2.10. +- Bumps minimum in_app_purchase_android to 0.2.3. ## 3.0.7 -* Fixes avoid_redundant_argument_values lint warnings and minor typos. +- Fixes avoid_redundant_argument_values lint warnings and minor typos. ## 3.0.6 -* Ignores deprecation warnings for upcoming styleFrom button API changes. +- Ignores deprecation warnings for upcoming styleFrom button API changes. ## 3.0.5 -* Updates references to the obsolete master branch. +- Updates references to the obsolete master branch. ## 3.0.4 -* Minor fixes for new analysis options. +- Minor fixes for new analysis options. ## 3.0.3 -* Removes unnecessary imports. -* Adds OS version support information to README. -* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors +- Removes unnecessary imports. +- Adds OS version support information to README. +- Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors lint warnings. ## 3.0.2 -* Adds additional explanation on why it is important to complete a purchase. +- Adds additional explanation on why it is important to complete a purchase. ## 3.0.1 -* Internal code cleanup for stricter analysis options. +- Internal code cleanup for stricter analysis options. ## 3.0.0 -* **BREAKING CHANGE** Updates `restorePurchases` to emit an empty list of purchases on StoreKit when there are no purchases to restore (same as Android). - * This change was listed in the CHANGELOG for 2.0.0, but the change was accidentally not included in 2.0.0. +- **BREAKING CHANGE** Updates `restorePurchases` to emit an empty list of purchases on StoreKit when there are no purchases to restore (same as Android). + - This change was listed in the CHANGELOG for 2.0.0, but the change was accidentally not included in 2.0.0. ## 2.0.1 -* Removes the instructions on initializing the plugin since this functionality is deprecated. +- Removes the instructions on initializing the plugin since this functionality is deprecated. ## 2.0.0 -* **BREAKING CHANGES**: - * Adds a new `PurchaseStatus` named `canceled`. This means developers can distinguish between an error and user cancellation. - * ~~Updates `restorePurchases` to emit an empty list of purchases on StoreKit when there are no purchases to restore (same as Android).~~ - * Renames `in_app_purchase_ios` to `in_app_purchase_storekit`. - * Renames `InAppPurchaseIosPlatform` to `InAppPurchaseStoreKitPlatform`. - * Renames `InAppPurchaseIosPlatformAddition` to +- **BREAKING CHANGES**: + + - Adds a new `PurchaseStatus` named `canceled`. This means developers can distinguish between an error and user cancellation. + - ~~Updates `restorePurchases` to emit an empty list of purchases on StoreKit when there are no purchases to restore (same as Android).~~ + - Renames `in_app_purchase_ios` to `in_app_purchase_storekit`. + - Renames `InAppPurchaseIosPlatform` to `InAppPurchaseStoreKitPlatform`. + - Renames `InAppPurchaseIosPlatformAddition` to `InAppPurchaseStoreKitPlatformAddition`. -* Deprecates the `InAppPurchaseAndroidPlatformAddition.enablePendingPurchases()` method and `InAppPurchaseAndroidPlatformAddition.enablePendingPurchase` property. -* Adds support for promotional offers on the store_kit_wrappers Dart API. -* Fixes integration tests. -* Updates example app Android compileSdkVersion to 31. +- Deprecates the `InAppPurchaseAndroidPlatformAddition.enablePendingPurchases()` method and `InAppPurchaseAndroidPlatformAddition.enablePendingPurchase` property. +- Adds support for promotional offers on the store_kit_wrappers Dart API. +- Fixes integration tests. +- Updates example app Android compileSdkVersion to 31. ## 1.0.9 -* Handle purchases with `PurchaseStatus.restored` correctly in the example App. -* Updated dependencies on `in_app_purchase_android` and `in_app_purchase_ios` to their latest versions (version 0.1.5 and 0.1.3+5 respectively). +- Handle purchases with `PurchaseStatus.restored` correctly in the example App. +- Updated dependencies on `in_app_purchase_android` and `in_app_purchase_ios` to their latest versions (version 0.1.5 and 0.1.3+5 respectively). ## 1.0.8 -* Fix repository link in pubspec.yaml. +- Fix repository link in pubspec.yaml. ## 1.0.7 -* Remove references to the Android V1 embedding. +- Remove references to the Android V1 embedding. ## 1.0.6 -* Added import flutter foundation dependency in README.md to be able to use `defaultTargetPlatform`. +- Added import flutter foundation dependency in README.md to be able to use `defaultTargetPlatform`. ## 1.0.5 -* Add explanation for casting `ProductDetails` and `PurchaseDetails` to platform specific implementations in the readme. +- Add explanation for casting `ProductDetails` and `PurchaseDetails` to platform specific implementations in the readme. ## 1.0.4 -* Fix `Restoring previous purchases` link in the README.md. +- Fix `Restoring previous purchases` link in the README.md. ## 1.0.3 -* Added a "Restore purchases" button to conform to Apple's StoreKit guidelines on [restoring products](https://developer.apple.com/documentation/storekit/in-app_purchase/restoring_purchased_products?language=objc); -* Corrected an error in a example snippet displayed in the README.md. +- Added a "Restore purchases" button to conform to Apple's StoreKit guidelines on [restoring products](https://developer.apple.com/documentation/storekit/in-app_purchase/restoring_purchased_products?language=objc); +- Corrected an error in a example snippet displayed in the README.md. ## 1.0.2 -* Fix ignoring "autoConsume" param in "InAppPurchase.instance.buyConsumable". +- Fix ignoring "autoConsume" param in "InAppPurchase.instance.buyConsumable". ## 1.0.1 -* Migrate maven repository from jcenter to mavenCentral. +- Migrate maven repository from jcenter to mavenCentral. ## 1.0.0 -* Stable release of in_app_purchase plugin. +- Stable release of in_app_purchase plugin. ## 0.6.0+1 -* Added a reference to the in-app purchase codelab in the README.md. +- Added a reference to the in-app purchase codelab in the README.md. ## 0.6.0 As part of implementing federated architecture and making the interface compatible for other platforms this version contains the following **breaking changes**: -* Changes to the platform agnostic interface: - * If you used `InAppPurchaseConnection.instance` to access generic In App Purchase APIs, please use `InAppPurchase.instance` instead; - * The `InAppPurchaseConnection.purchaseUpdatedStream` has been renamed to `InAppPurchase.purchaseStream`; - * The `InAppPurchaseConnection.queryPastPurchases` method has been removed. Instead, you should use `InAppPurchase.restorePurchases`. This method emits each restored purchase on the `InAppPurchase.purchaseStream`, the `PurchaseDetails` object will be marked with a `status` of `PurchaseStatus.restored`; - * The `InAppPurchase.completePurchase` method no longer returns an instance `BillingWrapperResult` class (which was Android specific). Instead it will return a completed `Future` if the method executed successfully, in case of errors it will complete with an `InAppPurchaseException` describing the error. -* Android specific changes: - * The Android specific `InAppPurchaseConnection.consumePurchase` and `InAppPurchaseConnection.enablePendingPurchases` methods have been removed from the platform agnostic interface and moved to the Android specific `InAppPurchaseAndroidPlatformAddition` class: - * `InAppPurchaseAndroidPlatformAddition.enablePendingPurchases` is a static method that should be called when initializing your App. Access the method like this: `InAppPurchaseAndroidPlatformAddition.enablePendingPurchases()` (make sure to add the following import: `import 'package:in_app_purchase_android/in_app_purchase_android.dart';`); - * To use the `InAppPurchaseAndroidPlatformAddition.consumePurchase` method, acquire an instance using the `InAppPurchase.getPlatformAddition` method. For example: +- Changes to the platform agnostic interface: + - If you used `InAppPurchaseConnection.instance` to access generic In App Purchase APIs, please use `InAppPurchase.instance` instead; + - The `InAppPurchaseConnection.purchaseUpdatedStream` has been renamed to `InAppPurchase.purchaseStream`; + - The `InAppPurchaseConnection.queryPastPurchases` method has been removed. Instead, you should use `InAppPurchase.restorePurchases`. This method emits each restored purchase on the `InAppPurchase.purchaseStream`, the `PurchaseDetails` object will be marked with a `status` of `PurchaseStatus.restored`; + - The `InAppPurchase.completePurchase` method no longer returns an instance `BillingWrapperResult` class (which was Android specific). Instead it will return a completed `Future` if the method executed successfully, in case of errors it will complete with an `InAppPurchaseException` describing the error. +- Android specific changes: + - The Android specific `InAppPurchaseConnection.consumePurchase` and `InAppPurchaseConnection.enablePendingPurchases` methods have been removed from the platform agnostic interface and moved to the Android specific `InAppPurchaseAndroidPlatformAddition` class: + - `InAppPurchaseAndroidPlatformAddition.enablePendingPurchases` is a static method that should be called when initializing your App. Access the method like this: `InAppPurchaseAndroidPlatformAddition.enablePendingPurchases()` (make sure to add the following import: `import 'package:in_app_purchase_android/in_app_purchase_android.dart';`); + - To use the `InAppPurchaseAndroidPlatformAddition.consumePurchase` method, acquire an instance using the `InAppPurchase.getPlatformAddition` method. For example: ```dart // Acquire the InAppPurchaseAndroidPlatformAddition instance. InAppPurchaseAndroidPlatformAddition androidAddition = InAppPurchase.instance.getPlatformAddition(); // Consume an Android purchase. BillingResultWrapper billingResult = await androidAddition.consumePurchase(purchase); ``` - * The [billing_client_wrappers](https://pub.dev/documentation/in_app_purchase_android/latest/billing_client_wrappers/billing_client_wrappers-library.html) have been moved into the [in_app_purchase_android](https://pub.dev/packages/in_app_purchase_android) package. They are still available through the [in_app_purchase](https://pub.dev/packages/in_app_purchase) plugin but to use them it is necessary to import the correct package when using them: `import 'package:in_app_purchase_android/billing_client_wrappers.dart';`; -* iOS specific changes: - * The iOS specific methods `InAppPurchaseConnection.presentCodeRedemptionSheet` and `InAppPurchaseConnection.refreshPurchaseVerificationData` methods have been removed from the platform agnostic interface and moved into the iOS specific `InAppPurchaseIosPlatformAddition` class. To use them acquire an instance through the `InAppPurchase.getPlatformAddition` method like so: + - The [billing_client_wrappers](https://pub.dev/documentation/in_app_purchase_android/latest/billing_client_wrappers/billing_client_wrappers-library.html) have been moved into the [in_app_purchase_android](https://pub.dev/packages/in_app_purchase_android) package. They are still available through the [in_app_purchase](https://pub.dev/packages/in_app_purchase) plugin but to use them it is necessary to import the correct package when using them: `import 'package:in_app_purchase_android/billing_client_wrappers.dart';`; +- iOS specific changes: + - The iOS specific methods `InAppPurchaseConnection.presentCodeRedemptionSheet` and `InAppPurchaseConnection.refreshPurchaseVerificationData` methods have been removed from the platform agnostic interface and moved into the iOS specific `InAppPurchaseIosPlatformAddition` class. To use them acquire an instance through the `InAppPurchase.getPlatformAddition` method like so: ```dart // Acquire the InAppPurchaseIosPlatformAddition instance. InAppPurchaseIosPlatformAddition iosAddition = InAppPurchase.instance.getPlatformAddition(); @@ -207,429 +209,429 @@ As part of implementing federated architecture and making the interface compatib // Refresh purchase verification data. PurchaseVerificationData? verificationData = await iosAddition.refreshPurchaseVerificationData(); ``` - * The [store_kit_wrappers](https://pub.dev/documentation/in_app_purchase_ios/latest/store_kit_wrappers/store_kit_wrappers-library.html) have been moved into the [in_app_purchase_ios](https://pub.dev/packages/in_app_purchase_ios) package. They are still available in the [in_app_purchase](https://pub.dev/packages/in_app_purchase) plugin, but to use them it is necessary to import the correct package when using them: `import 'package:in_app_purchase_ios/store_kit_wrappers.dart';`; - * Update the minimum supported Flutter version to 1.20.0. + - The [store_kit_wrappers](https://pub.dev/documentation/in_app_purchase_ios/latest/store_kit_wrappers/store_kit_wrappers-library.html) have been moved into the [in_app_purchase_ios](https://pub.dev/packages/in_app_purchase_ios) package. They are still available in the [in_app_purchase](https://pub.dev/packages/in_app_purchase) plugin, but to use them it is necessary to import the correct package when using them: `import 'package:in_app_purchase_ios/store_kit_wrappers.dart';`; + - Update the minimum supported Flutter version to 1.20.0. ## 0.5.2 -* Added `rawPrice` and `currencyCode` to the ProductDetails model. +- Added `rawPrice` and `currencyCode` to the ProductDetails model. ## 0.5.1+3 -* Configured the iOS example App to make use of StoreKit Testing on iOS 14 and higher. +- Configured the iOS example App to make use of StoreKit Testing on iOS 14 and higher. ## 0.5.1+2 -* Update README to provide a better instruction of the plugin. +- Update README to provide a better instruction of the plugin. ## 0.5.1+1 -* Fix error message when trying to consume purchase on iOS. +- Fix error message when trying to consume purchase on iOS. ## 0.5.1 -* [iOS] Introduce `SKPaymentQueueWrapper.presentCodeRedemptionSheet` +- [iOS] Introduce `SKPaymentQueueWrapper.presentCodeRedemptionSheet` ## 0.5.0 -* Migrate to Google Billing Library 3.0 - * Add `obfuscatedProfileId`, `purchaseToken` in [BillingClientWrapper.launchBillingFlow]. - * **Breaking Change** - * Removed `developerPayload` in [BillingClientWrapper.acknowledgePurchase], [BillingClientWrapper.consumeAsync], [InAppPurchaseConnection.completePurchase], [InAppPurchaseConnection.consumePurchase]. - * Removed `isRewarded` from [SkuDetailsWrapper]. - * [SkuDetailsWrapper.introductoryPriceCycles] now returns `int` instead of `String`. - * Above breaking changes are inline with the breaking changes introduced in [Google Play Billing 3.0 release](https://developer.android.com/google/play/billing/release-notes#3-0). - * Additional information on some the changes: - * [Dropping reward SKU support](https://support.google.com/googleplay/android-developer/answer/9155268?hl=en) - * [Developer payload](https://developer.android.com/google/play/billing/developer-payload) +- Migrate to Google Billing Library 3.0 + - Add `obfuscatedProfileId`, `purchaseToken` in [BillingClientWrapper.launchBillingFlow]. + - **Breaking Change** + - Removed `developerPayload` in [BillingClientWrapper.acknowledgePurchase], [BillingClientWrapper.consumeAsync], [InAppPurchaseConnection.completePurchase], [InAppPurchaseConnection.consumePurchase]. + - Removed `isRewarded` from [SkuDetailsWrapper]. + - [SkuDetailsWrapper.introductoryPriceCycles] now returns `int` instead of `String`. + - Above breaking changes are inline with the breaking changes introduced in [Google Play Billing 3.0 release](https://developer.android.com/google/play/billing/release-notes#3-0). + - Additional information on some the changes: + - [Dropping reward SKU support](https://support.google.com/googleplay/android-developer/answer/9155268?hl=en) + - [Developer payload](https://developer.android.com/google/play/billing/developer-payload) ## 0.4.1 -* Support InApp subscription upgrade/downgrade. +- Support InApp subscription upgrade/downgrade. ## 0.4.0 -* Migrate to nullsafety. -* Deprecate `sandboxTesting`, introduce `simulatesAskToBuyInSandbox`. -* **Breaking Change:** - * Removed `callbackChannel` in `channels.dart`, see https://github.com/flutter/flutter/issues/69225. +- Migrate to nullsafety. +- Deprecate `sandboxTesting`, introduce `simulatesAskToBuyInSandbox`. +- **Breaking Change:** + - Removed `callbackChannel` in `channels.dart`, see https://github.com/flutter/flutter/issues/69225. ## 0.3.5+2 -* Migrate deprecated references. +- Migrate deprecated references. ## 0.3.5+1 -* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. +- Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. ## 0.3.5 -* [Android] Fixed: added support for the SERVICE_TIMEOUT (-3) response code. +- [Android] Fixed: added support for the SERVICE_TIMEOUT (-3) response code. ## 0.3.4+18 -* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) +- Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) ## 0.3.4+17 -* Update Flutter SDK constraint. +- Update Flutter SDK constraint. ## 0.3.4+16 -* Add Dartdocs to all public APIs. +- Add Dartdocs to all public APIs. ## 0.3.4+15 -* Update android compileSdkVersion to 29. +- Update android compileSdkVersion to 29. ## 0.3.4+14 -* Add test target to iOS example app Podfile +- Add test target to iOS example app Podfile ## 0.3.4+13 -* Android Code Inspection and Clean up. +- Android Code Inspection and Clean up. ## 0.3.4+12 -* [iOS] Fixed: finishing purchases upon payment dialog cancellation. +- [iOS] Fixed: finishing purchases upon payment dialog cancellation. ## 0.3.4+11 -* [iOS] Fixed: crash when sending null for simulatesAskToBuyInSandbox parameter. +- [iOS] Fixed: crash when sending null for simulatesAskToBuyInSandbox parameter. ## 0.3.4+10 -* Fixed typo 'verity' for 'verify'. +- Fixed typo 'verity' for 'verify'. ## 0.3.4+9 -* [iOS] Fixed: purchase dialog not showing always. -* [iOS] Fixed: completing purchases could fail. -* [iOS] Fixed: restorePurchases caused hang (call never returned). +- [iOS] Fixed: purchase dialog not showing always. +- [iOS] Fixed: completing purchases could fail. +- [iOS] Fixed: restorePurchases caused hang (call never returned). ## 0.3.4+8 -* [iOS] Fixed: purchase dialog not showing always. -* [iOS] Fixed: completing purchases could fail. -* [iOS] Fixed: restorePurchases caused hang (call never returned). +- [iOS] Fixed: purchase dialog not showing always. +- [iOS] Fixed: completing purchases could fail. +- [iOS] Fixed: restorePurchases caused hang (call never returned). ## 0.3.4+7 -* iOS: Fix typo of the `simulatesAskToBuyInSandbox` key. +- iOS: Fix typo of the `simulatesAskToBuyInSandbox` key. ## 0.3.4+6 -* iOS: Fix the bug that prevent restored subscription transactions from being completed +- iOS: Fix the bug that prevent restored subscription transactions from being completed ## 0.3.4+5 -* Added necessary README docs for getting started with Android. +- Added necessary README docs for getting started with Android. ## 0.3.4+4 -* Update package:e2e -> package:integration_test +- Update package:e2e -> package:integration_test ## 0.3.4+3 -* Fixed typo 'manuelly' for 'manually'. +- Fixed typo 'manuelly' for 'manually'. ## 0.3.4+2 -* Update package:e2e reference to use the local version in the flutter/plugins +- Update package:e2e reference to use the local version in the flutter/plugins repository. ## 0.3.4+1 -* iOS: Fix the bug that `SKPaymentQueueWrapper.transactions` doesn't return all transactions. -* iOS: Fix the app crashes if `InAppPurchaseConnection.instance` is called in the `main()`. +- iOS: Fix the bug that `SKPaymentQueueWrapper.transactions` doesn't return all transactions. +- iOS: Fix the app crashes if `InAppPurchaseConnection.instance` is called in the `main()`. ## 0.3.4 -* Expose SKError code to client apps. +- Expose SKError code to client apps. ## 0.3.3+2 -* Post-v2 Android embedding cleanups. +- Post-v2 Android embedding cleanups. ## 0.3.3+1 -* Update documentations for `InAppPurchase.completePurchase` and update README. +- Update documentations for `InAppPurchase.completePurchase` and update README. ## 0.3.3 -* Introduce `SKPaymentQueueWrapper.transactions`. +- Introduce `SKPaymentQueueWrapper.transactions`. ## 0.3.2+2 -* Fix CocoaPods podspec lint warnings. +- Fix CocoaPods podspec lint warnings. ## 0.3.2+1 -* iOS: Fix only transactions with SKPaymentTransactionStatePurchased and SKPaymentTransactionStateFailed can be finished. -* iOS: Only one pending transaction of a given product is allowed. +- iOS: Fix only transactions with SKPaymentTransactionStatePurchased and SKPaymentTransactionStateFailed can be finished. +- iOS: Only one pending transaction of a given product is allowed. ## 0.3.2 -* Remove Android dependencies fallback. -* Require Flutter SDK 1.12.13+hotfix.5 or greater. +- Remove Android dependencies fallback. +- Require Flutter SDK 1.12.13+hotfix.5 or greater. ## 0.3.1+2 -* Fix potential casting crash on Android v1 embedding when registering life cycle callbacks. -* Remove hard-coded legacy xcode build setting. +- Fix potential casting crash on Android v1 embedding when registering life cycle callbacks. +- Remove hard-coded legacy xcode build setting. ## 0.3.1+1 -* Add `pedantic` to dev_dependency. +- Add `pedantic` to dev_dependency. ## 0.3.1 -* Android: Fix a bug where the `BillingClient` is disconnected when app goes to the background. -* Android: Make sure the `BillingClient` object is disconnected before the activity is destroyed. -* Android: Fix minor compiler warning. -* Fix typo in CHANGELOG. +- Android: Fix a bug where the `BillingClient` is disconnected when app goes to the background. +- Android: Make sure the `BillingClient` object is disconnected before the activity is destroyed. +- Android: Fix minor compiler warning. +- Fix typo in CHANGELOG. ## 0.3.0+3 -* Fix pendingCompletePurchase flag status to allow to complete purchases. +- Fix pendingCompletePurchase flag status to allow to complete purchases. ## 0.3.0+2 -* Update te example app to avoid using deprecated api. +- Update te example app to avoid using deprecated api. ## 0.3.0+1 -* Fixing usage example. No functional changes. +- Fixing usage example. No functional changes. ## 0.3.0 -* Migrate the `Google Play Library` to 2.0.3. - * Introduce a new class `BillingResultWrapper` which contains a detailed result of a BillingClient operation. - * **[Breaking Change]:** All the BillingClient methods that previously return a `BillingResponse` now return a `BillingResultWrapper`, including: `launchBillingFlow`, `startConnection` and `consumeAsync`. - * **[Breaking Change]:** The `SkuDetailsResponseWrapper` now contains a `billingResult` field in place of `billingResponse` field. - * A `billingResult` field is added to the `PurchasesResultWrapper`. - * Other Updates to the "billing_client_wrappers": - * Updates to the `PurchaseWrapper`: Add `developerPayload`, `purchaseState` and `isAcknowledged` fields. - * Updates to the `SkuDetailsWrapper`: Add `originalPrice` and `originalPriceAmountMicros` fields. - * **[Breaking Change]:** The `BillingClient.queryPurchaseHistory` is updated to return a `PurchasesHistoryResult`, which contains a list of `PurchaseHistoryRecordWrapper` instead of `PurchaseWrapper`. A `PurchaseHistoryRecordWrapper` object has the same fields and values as A `PurchaseWrapper` object, except that a `PurchaseHistoryRecordWrapper` object does not contain `isAutoRenewing`, `orderId` and `packageName`. - * Add a new `BillingClient.acknowledgePurchase` API. Starting from this version, the developer has to acknowledge any purchase on Android using this API within 3 days of purchase, or the user will be refunded. Note that if a product is "consumed" via `BillingClient.consumeAsync`, it is implicitly acknowledged. - * **[Breaking Change]:** Added `enablePendingPurchases` in `BillingClientWrapper`. The application has to call this method before calling `BillingClientWrapper.startConnection`. See [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) for more information. - * Updates to the "InAppPurchaseConnection": - * **[Breaking Change]:** `InAppPurchaseConnection.completePurchase` now returns a `Future` instead of `Future`. A new optional parameter `{String developerPayload}` has also been added to the API. On Android, this API does not throw an exception anymore, it instead acknowledge the purchase. If a purchase is not completed within 3 days on Android, the user will be refunded. - * **[Breaking Change]:** `InAppPurchaseConnection.consumePurchase` now returns a `Future` instead of `Future`. A new optional parameter `{String developerPayload}` has also been added to the API. - * A new boolean field `pendingCompletePurchase` has been added to the `PurchaseDetails` class. Which can be used as an indicator of whether to call `InAppPurchaseConnection.completePurchase` on the purchase. - * **[Breaking Change]:** Added `enablePendingPurchases` in `InAppPurchaseConnection`. The application has to call this method when initializing the `InAppPurchaseConnection` on Android. See [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) for more information. - * Misc: Some documentation updates reflecting the `BillingClient` migration and some documentation fixes. - * Refer to [Google Play Billing Library Release Note](https://developer.android.com/google/play/billing/billing_library_releases_notes#release-2_0) for a detailed information on the update. +- Migrate the `Google Play Library` to 2.0.3. + - Introduce a new class `BillingResultWrapper` which contains a detailed result of a BillingClient operation. + - **[Breaking Change]:** All the BillingClient methods that previously return a `BillingResponse` now return a `BillingResultWrapper`, including: `launchBillingFlow`, `startConnection` and `consumeAsync`. + - **[Breaking Change]:** The `SkuDetailsResponseWrapper` now contains a `billingResult` field in place of `billingResponse` field. + - A `billingResult` field is added to the `PurchasesResultWrapper`. + - Other Updates to the "billing_client_wrappers": + - Updates to the `PurchaseWrapper`: Add `developerPayload`, `purchaseState` and `isAcknowledged` fields. + - Updates to the `SkuDetailsWrapper`: Add `originalPrice` and `originalPriceAmountMicros` fields. + - **[Breaking Change]:** The `BillingClient.queryPurchaseHistory` is updated to return a `PurchasesHistoryResult`, which contains a list of `PurchaseHistoryRecordWrapper` instead of `PurchaseWrapper`. A `PurchaseHistoryRecordWrapper` object has the same fields and values as A `PurchaseWrapper` object, except that a `PurchaseHistoryRecordWrapper` object does not contain `isAutoRenewing`, `orderId` and `packageName`. + - Add a new `BillingClient.acknowledgePurchase` API. Starting from this version, the developer has to acknowledge any purchase on Android using this API within 3 days of purchase, or the user will be refunded. Note that if a product is "consumed" via `BillingClient.consumeAsync`, it is implicitly acknowledged. + - **[Breaking Change]:** Added `enablePendingPurchases` in `BillingClientWrapper`. The application has to call this method before calling `BillingClientWrapper.startConnection`. See [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) for more information. + - Updates to the "InAppPurchaseConnection": + - **[Breaking Change]:** `InAppPurchaseConnection.completePurchase` now returns a `Future` instead of `Future`. A new optional parameter `{String developerPayload}` has also been added to the API. On Android, this API does not throw an exception anymore, it instead acknowledge the purchase. If a purchase is not completed within 3 days on Android, the user will be refunded. + - **[Breaking Change]:** `InAppPurchaseConnection.consumePurchase` now returns a `Future` instead of `Future`. A new optional parameter `{String developerPayload}` has also been added to the API. + - A new boolean field `pendingCompletePurchase` has been added to the `PurchaseDetails` class. Which can be used as an indicator of whether to call `InAppPurchaseConnection.completePurchase` on the purchase. + - **[Breaking Change]:** Added `enablePendingPurchases` in `InAppPurchaseConnection`. The application has to call this method when initializing the `InAppPurchaseConnection` on Android. See [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) for more information. + - Misc: Some documentation updates reflecting the `BillingClient` migration and some documentation fixes. + - Refer to [Google Play Billing Library Release Note](https://developer.android.com/google/play/billing/billing_library_releases_notes#release-2_0) for a detailed information on the update. ## 0.2.2+6 -* Correct a comment. +- Correct a comment. ## 0.2.2+5 -* Update version of json_annotation to ^3.0.0 and json_serializable to ^3.2.0. Resolve conflicts with other packages e.g. flutter_tools from sdk. +- Update version of json_annotation to ^3.0.0 and json_serializable to ^3.2.0. Resolve conflicts with other packages e.g. flutter_tools from sdk. ## 0.2.2+4 -* Remove the deprecated `author:` field from pubspec.yaml -* Migrate the plugin to the pubspec platforms manifest. -* Require Flutter SDK 1.10.0 or greater. +- Remove the deprecated `author:` field from pubspec.yaml +- Migrate the plugin to the pubspec platforms manifest. +- Require Flutter SDK 1.10.0 or greater. ## 0.2.2+3 -* Fix failing pedantic lints. None of these fixes should have any change in +- Fix failing pedantic lints. None of these fixes should have any change in functionality. ## 0.2.2+2 -* Include lifecycle dependency as a compileOnly one on Android to resolve +- Include lifecycle dependency as a compileOnly one on Android to resolve potential version conflicts with other transitive libraries. ## 0.2.2+1 -* Android: Use android.arch.lifecycle instead of androidx.lifecycle:lifecycle in `build.gradle` to support apps that has not been migrated to AndroidX. +- Android: Use android.arch.lifecycle instead of androidx.lifecycle:lifecycle in `build.gradle` to support apps that has not been migrated to AndroidX. ## 0.2.2 -* Support the v2 Android embedder. -* Update to AndroidX. -* Migrate to using the new e2e test binding. -* Add a e2e test. +- Support the v2 Android embedder. +- Update to AndroidX. +- Migrate to using the new e2e test binding. +- Add a e2e test. ## 0.2.1+5 -* Define clang module for iOS. -* Fix iOS build warning. +- Define clang module for iOS. +- Fix iOS build warning. ## 0.2.1+4 -* Update and migrate iOS example project. +- Update and migrate iOS example project. ## 0.2.1+3 -* Android : Improved testability. +- Android : Improved testability. ## 0.2.1+2 -* Android: Require a non-null Activity to use the `launchBillingFlow` method. +- Android: Require a non-null Activity to use the `launchBillingFlow` method. ## 0.2.1+1 -* Remove skipped driver test. +- Remove skipped driver test. ## 0.2.1 -* iOS: Add currencyCode to priceLocale on productDetails. +- iOS: Add currencyCode to priceLocale on productDetails. ## 0.2.0+8 -* Add dependency on `androidx.annotation:annotation:1.0.0`. +- Add dependency on `androidx.annotation:annotation:1.0.0`. ## 0.2.0+7 -* Make Gradle version compatible with the Android Gradle plugin version. +- Make Gradle version compatible with the Android Gradle plugin version. ## 0.2.0+6 -* Add missing `hashCode` implementations. +- Add missing `hashCode` implementations. ## 0.2.0+5 -* iOS: Support unsupported UserInfo value types on NSError. +- iOS: Support unsupported UserInfo value types on NSError. ## 0.2.0+4 -* Fixed code error in `README.md` and adjusted links to work on Pub. +- Fixed code error in `README.md` and adjusted links to work on Pub. ## 0.2.0+3 -* Update the `README.md` so that the code samples compile with the latest Flutter/Dart version. +- Update the `README.md` so that the code samples compile with the latest Flutter/Dart version. ## 0.2.0+2 -* Fix a google_play_connection purchase update listener regression introduced in 0.2.0+1. +- Fix a google_play_connection purchase update listener regression introduced in 0.2.0+1. ## 0.2.0+1 -* Fix an issue the type is not casted before passing to `PurchasesResultWrapper.fromJson`. +- Fix an issue the type is not casted before passing to `PurchasesResultWrapper.fromJson`. ## 0.2.0 -* [Breaking Change] Rename 'PurchaseError' to 'IAPError'. -* [Breaking Change] Rename 'PurchaseSource' to 'IAPSource'. +- [Breaking Change] Rename 'PurchaseError' to 'IAPError'. +- [Breaking Change] Rename 'PurchaseSource' to 'IAPSource'. ## 0.1.1+3 -* Expanded description in `pubspec.yaml` and fixed typo in `README.md`. +- Expanded description in `pubspec.yaml` and fixed typo in `README.md`. ## 0.1.1+2 -* Add missing template type parameter to `invokeMethod` calls. -* Bump minimum Flutter version to 1.5.0. -* Replace invokeMethod with invokeMapMethod wherever necessary. +- Add missing template type parameter to `invokeMethod` calls. +- Bump minimum Flutter version to 1.5.0. +- Replace invokeMethod with invokeMapMethod wherever necessary. ## 0.1.1+1 -* Make `AdditionalSteps`(Used in the unit test) a void function. +- Make `AdditionalSteps`(Used in the unit test) a void function. ## 0.1.1 -* Some error messages from iOS are slightly changed. -* `ProductDetailsResponse` returned by `queryProductDetails()` now contains an `PurchaseError` object that represents any error that might occurred during the request. -* If the device is not connected to the internet, `queryPastPurchases()` on iOS now have the error stored in the response instead of throwing. -* Clean up minor iOS warning. -* Example app shows how to handle error when calling `queryProductDetails()` and `queryProductDetails()`. +- Some error messages from iOS are slightly changed. +- `ProductDetailsResponse` returned by `queryProductDetails()` now contains an `PurchaseError` object that represents any error that might occurred during the request. +- If the device is not connected to the internet, `queryPastPurchases()` on iOS now have the error stored in the response instead of throwing. +- Clean up minor iOS warning. +- Example app shows how to handle error when calling `queryProductDetails()` and `queryProductDetails()`. ## 0.1.0+4 -* Change the `buy` methods to return `Future` instead of `void` in order +- Change the `buy` methods to return `Future` instead of `void` in order to propagate `launchBillingFlow` failures up through `google_play_connection`. ## 0.1.0+3 -* Guard against multiple onSetupFinished() calls. +- Guard against multiple onSetupFinished() calls. ## 0.1.0+2 -* Fix bug where error only purchases updates weren't propagated correctly in +- Fix bug where error only purchases updates weren't propagated correctly in `google_play_connection.dart`. ## 0.1.0+1 -* Add more consumable handling to the example app. +- Add more consumable handling to the example app. ## 0.1.0 Beta release. -* Ability to list products, load previous purchases, and make purchases. -* Simplified Dart API that's been unified for ease of use. -* Platform-specific APIs more directly exposing `StoreKit` and `BillingClient`. +- Ability to list products, load previous purchases, and make purchases. +- Simplified Dart API that's been unified for ease of use. +- Platform-specific APIs more directly exposing `StoreKit` and `BillingClient`. Includes: -* 5ba657dc [in_app_purchase] Remove extraneous download logic (#1560) -* 01bb8796 [in_app_purchase] Minor doc updates (#1555) -* 1a4d493f [in_app_purchase] Only fetch owned purchases (#1540) -* d63c51cf [in_app_purchase] Add auto-consume errors to PurchaseDetails (#1537) -* 959da97f [in_app_purchase] Minor doc updates (#1536) -* b82ae1a6 [in_app_purchase] Rename the unified API (#1517) -* d1ad723a [in_app_purchase]remove SKDownloadWrapper and related code. (#1474) -* 7c1e8b8a [in_app_purchase]make payment unified APIs (#1421) -* 80233db6 [in_app_purchase] Add references to the original object for PurchaseDetails and ProductDetails (#1448) -* 8c180f0d [in_app_purchase]load purchase (#1380) -* e9f141bc [in_app_purchase] Iap refactor (#1381) -* d3b3d60c add driver test command to cirrus (#1342) -* aee12523 [in_app_purchase] refactoring and tests (#1322) -* 6d7b4592 [in_app_purchase] Adds Dart BillingClient APIs for loading purchases (#1286) -* 5567a9c8 [in_app_purchase]retrieve receipt (#1303) -* 3475f1b7 [in_app_purchase]restore purchases (#1299) -* a533148d [in_app_purchase] payment queue dart ios (#1249) -* 10030840 [in_app_purchase] Minor bugfixes and code cleanup (#1284) -* 347f508d [in_app_purchase] Fix CI formatting errors. (#1281) -* fad02d87 [in_app_purchase] Java API for querying purchases (#1259) -* bc501915 [In_app_purchase]SKProduct related fixes (#1252) -* f92ba3a1 IAP make payment objc (#1231) -* 62b82522 [IAP] Add the Dart API for launchBillingFlow (#1232) -* b40a4acf [IAP] Add Java call for launchBillingFlow (#1230) -* 4ff06cd1 [In_app_purchase]remove categories (#1222) -* 0e72ca56 [In_app_purchase]fix requesthandler crash (#1199) -* 81dff2be Iap getproductlist basic draft (#1169) -* db139b28 Iap iOS add payment dart wrappers (#1178) -* 2e5fbb9b Fix the param map passed down to the platform channel when calling querySkuDetails (#1194) -* 4a84bac1 Mark some packages as unpublishable (#1193) -* 51696552 Add a gradle warning to the AndroidX plugins (#1138) -* 832ab832 Iap add payment objc translators (#1172) -* d0e615cf Revert "IAP add payment translators in objc (#1126)" (#1171) -* 09a5a36e IAP add payment translators in objc (#1126) -* a100fbf9 Expose nslocale and expose currencySymbol instead of currencyCode to match android (#1162) -* 1c982efd Using json serializer for skproduct wrapper and related classes (#1147) -* 3039a261 Iap productlist ios (#1068) -* 2a1593da [IAP] Update dev deps to match flutter_driver (#1118) -* 9f87cbe5 [IAP] Update README (#1112) -* 59e84d85 Migrate independent plugins to AndroidX (#1103) -* a027ccd6 [IAP] Generate boilerplate serializers (#1090) -* 909cf1c2 [IAP] Fetch SkuDetails from Google Play (#1084) -* 6bbaa7e5 [IAP] Add missing license headers (#1083) -* 5347e877 [IAP] Clean up Dart unit tests (#1082) -* fe03e407 [IAP] Check if the payment processor is available (#1057) -* 43ee28cf Fix `Manifest versionCode not found` (#1076) -* 4d702ad7 Supress `strong_mode_implicit_dynamic_method` for `invokeMethod` calls. (#1065) -* 809ccde7 Doc and build script updates to the IAP plugin (#1024) -* 052b71a9 Update the IAP README (#933) -* 54f9c4e2 Upgrade Android Gradle Plugin to 3.2.1 (#916) -* ced3e99d Set all gradle-wrapper versions to 4.10.2 (#915) -* eaa1388b Reconfigure Cirrus to use clang 7 (#905) -* 9b153920 Update gradle dependencies. (#881) -* 1aef7d92 Enable lint unnecessary_new (#701) +- 5ba657dc [in_app_purchase] Remove extraneous download logic (#1560) +- 01bb8796 [in_app_purchase] Minor doc updates (#1555) +- 1a4d493f [in_app_purchase] Only fetch owned purchases (#1540) +- d63c51cf [in_app_purchase] Add auto-consume errors to PurchaseDetails (#1537) +- 959da97f [in_app_purchase] Minor doc updates (#1536) +- b82ae1a6 [in_app_purchase] Rename the unified API (#1517) +- d1ad723a [in_app_purchase]remove SKDownloadWrapper and related code. (#1474) +- 7c1e8b8a [in_app_purchase]make payment unified APIs (#1421) +- 80233db6 [in_app_purchase] Add references to the original object for PurchaseDetails and ProductDetails (#1448) +- 8c180f0d [in_app_purchase]load purchase (#1380) +- e9f141bc [in_app_purchase] Iap refactor (#1381) +- d3b3d60c add driver test command to cirrus (#1342) +- aee12523 [in_app_purchase] refactoring and tests (#1322) +- 6d7b4592 [in_app_purchase] Adds Dart BillingClient APIs for loading purchases (#1286) +- 5567a9c8 [in_app_purchase]retrieve receipt (#1303) +- 3475f1b7 [in_app_purchase]restore purchases (#1299) +- a533148d [in_app_purchase] payment queue dart ios (#1249) +- 10030840 [in_app_purchase] Minor bugfixes and code cleanup (#1284) +- 347f508d [in_app_purchase] Fix CI formatting errors. (#1281) +- fad02d87 [in_app_purchase] Java API for querying purchases (#1259) +- bc501915 [In_app_purchase]SKProduct related fixes (#1252) +- f92ba3a1 IAP make payment objc (#1231) +- 62b82522 [IAP] Add the Dart API for launchBillingFlow (#1232) +- b40a4acf [IAP] Add Java call for launchBillingFlow (#1230) +- 4ff06cd1 [In_app_purchase]remove categories (#1222) +- 0e72ca56 [In_app_purchase]fix requesthandler crash (#1199) +- 81dff2be Iap getproductlist basic draft (#1169) +- db139b28 Iap iOS add payment dart wrappers (#1178) +- 2e5fbb9b Fix the param map passed down to the platform channel when calling querySkuDetails (#1194) +- 4a84bac1 Mark some packages as unpublishable (#1193) +- 51696552 Add a gradle warning to the AndroidX plugins (#1138) +- 832ab832 Iap add payment objc translators (#1172) +- d0e615cf Revert "IAP add payment translators in objc (#1126)" (#1171) +- 09a5a36e IAP add payment translators in objc (#1126) +- a100fbf9 Expose nslocale and expose currencySymbol instead of currencyCode to match android (#1162) +- 1c982efd Using json serializer for skproduct wrapper and related classes (#1147) +- 3039a261 Iap productlist ios (#1068) +- 2a1593da [IAP] Update dev deps to match flutter_driver (#1118) +- 9f87cbe5 [IAP] Update README (#1112) +- 59e84d85 Migrate independent plugins to AndroidX (#1103) +- a027ccd6 [IAP] Generate boilerplate serializers (#1090) +- 909cf1c2 [IAP] Fetch SkuDetails from Google Play (#1084) +- 6bbaa7e5 [IAP] Add missing license headers (#1083) +- 5347e877 [IAP] Clean up Dart unit tests (#1082) +- fe03e407 [IAP] Check if the payment processor is available (#1057) +- 43ee28cf Fix `Manifest versionCode not found` (#1076) +- 4d702ad7 Supress `strong_mode_implicit_dynamic_method` for `invokeMethod` calls. (#1065) +- 809ccde7 Doc and build script updates to the IAP plugin (#1024) +- 052b71a9 Update the IAP README (#933) +- 54f9c4e2 Upgrade Android Gradle Plugin to 3.2.1 (#916) +- ced3e99d Set all gradle-wrapper versions to 4.10.2 (#915) +- eaa1388b Reconfigure Cirrus to use clang 7 (#905) +- 9b153920 Update gradle dependencies. (#881) +- 1aef7d92 Enable lint unnecessary_new (#701) ## 0.0.2 -* Added missing flutter_test package dependency. -* Added missing flutter version requirements. +- Added missing flutter_test package dependency. +- Added missing flutter version requirements. ## 0.0.1 -* Initial release. +- Initial release. diff --git a/packages/in_app_purchase/in_app_purchase/README.md b/packages/in_app_purchase/in_app_purchase/README.md index 9242ecee44d7..6340db25b314 100644 --- a/packages/in_app_purchase/in_app_purchase/README.md +++ b/packages/in_app_purchase/in_app_purchase/README.md @@ -216,7 +216,7 @@ the end user's payment account. To upgrade/downgrade an existing in-app subscription in Google Play, you need to provide an instance of `ChangeSubscriptionParam` with the old `PurchaseDetails` that the user needs to migrate from, and an optional -`ProrationMode` with the `GooglePlayPurchaseParam` object while calling +`ReplacementMode` with the `GooglePlayPurchaseParam` object while calling `InAppPurchase.buyNonConsumable`. The App Store does not require this because it provides a subscription @@ -232,7 +232,7 @@ PurchaseParam purchaseParam = GooglePlayPurchaseParam( productDetails: productDetails, changeSubscriptionParam: ChangeSubscriptionParam( oldPurchaseDetails: oldPurchaseDetails, - prorationMode: ProrationMode.immediateWithTimeProration)); + replacementMode: ReplacementMode.withTimeProration)); InAppPurchase.instance .buyNonConsumable(purchaseParam: purchaseParam); ``` diff --git a/packages/in_app_purchase/in_app_purchase/example/lib/main.dart b/packages/in_app_purchase/in_app_purchase/example/lib/main.dart index 5afb52ac99ea..19698ff8a9c9 100644 --- a/packages/in_app_purchase/in_app_purchase/example/lib/main.dart +++ b/packages/in_app_purchase/in_app_purchase/example/lib/main.dart @@ -284,8 +284,8 @@ class _MyAppState extends State<_MyApp> { changeSubscriptionParam: (oldSubscription != null) ? ChangeSubscriptionParam( oldPurchaseDetails: oldSubscription, - prorationMode: - ProrationMode.immediateWithTimeProration, + replacementMode: + ReplacementMode.withTimeProration, ) : null); } else { 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 bfa7d76bca0d..d9f78b36231a 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,13 @@ +## 0.4.0 + +* Updates Google Play Billing Library to 7.1.1. +* **BREAKING CHANGES**: + * Removes the deprecated `ProrationMode` enum. `ReplacementMode` should be used instead. + * Removes the deprecated `BillingClientWrapper.enablePendingPurchases` method. + * Bumps `minSdk` to 21 +* Adds `installmentPlanDetails` to `SubscriptionOfferDetailsWrapper` +* Adds APIs to support pending transactions for subscription prepaid plans (`PendingPurchaseParams`). + ## 0.3.6+12 * Updates README to remove contributor-focused documentation. 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 7a790d8d8406..e440b1976171 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 @@ -31,7 +31,7 @@ android { compileSdk 34 defaultConfig { - minSdk 19 + minSdk 21 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { @@ -60,7 +60,7 @@ android { dependencies { implementation 'androidx.annotation:annotation:1.9.0' - implementation 'com.android.billingclient:billing:6.2.0' + implementation 'com.android.billingclient:billing:7.1.1' testImplementation 'junit:junit:4.13.2' testImplementation 'org.json:json:20240303' testImplementation 'org.mockito:mockito-core:5.4.0' diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java index 979e54a37bbf..56e6f8c39654 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java @@ -24,5 +24,6 @@ interface BillingClientFactory { BillingClient createBillingClient( @NonNull Context context, @NonNull Messages.InAppPurchaseCallbackApi callbackApi, - PlatformBillingChoiceMode billingChoiceMode); + PlatformBillingChoiceMode billingChoiceMode, + Messages.PendingPurchasesParams pendingPurchasesParams); } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java index 460414b2e5ba..4b567c847f00 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java @@ -10,6 +10,7 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.billingclient.api.BillingClient; +import com.android.billingclient.api.PendingPurchasesParams; import com.android.billingclient.api.UserChoiceBillingListener; import io.flutter.Log; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingChoiceMode; @@ -21,8 +22,16 @@ final class BillingClientFactoryImpl implements BillingClientFactory { public BillingClient createBillingClient( @NonNull Context context, @NonNull Messages.InAppPurchaseCallbackApi callbackApi, - PlatformBillingChoiceMode billingChoiceMode) { - BillingClient.Builder builder = BillingClient.newBuilder(context).enablePendingPurchases(); + PlatformBillingChoiceMode billingChoiceMode, + Messages.PendingPurchasesParams pendingPurchasesParams) { + PendingPurchasesParams.Builder pendingPurchasesBuilder = + PendingPurchasesParams.newBuilder().enableOneTimeProducts(); + if (pendingPurchasesParams != null && pendingPurchasesParams.getEnablePrepaidPlans()) { + pendingPurchasesBuilder.enablePrepaidPlans(); + } + + BillingClient.Builder builder = + BillingClient.newBuilder(context).enablePendingPurchases(pendingPurchasesBuilder.build()); switch (billingChoiceMode) { case ALTERNATIVE_BILLING_ONLY: // https://developer.android.com/google/play/billing/alternative/alternative-billing-without-user-choice-in-app diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java index cef3e316c3d6..240c8b1dc81b 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Messages.java @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.4.2), do not edit directly. +// Autogenerated from Pigeon (v22.6.1), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.inapppurchase; @@ -1086,19 +1086,6 @@ public void setProduct(@NonNull String setterArg) { this.product = setterArg; } - private @NonNull Long prorationMode; - - public @NonNull Long getProrationMode() { - return prorationMode; - } - - public void setProrationMode(@NonNull Long setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"prorationMode\" is null."); - } - this.prorationMode = setterArg; - } - private @NonNull Long replacementMode; public @NonNull Long getReplacementMode() { @@ -1175,7 +1162,6 @@ public boolean equals(Object o) { } PlatformBillingFlowParams that = (PlatformBillingFlowParams) o; return product.equals(that.product) - && prorationMode.equals(that.prorationMode) && replacementMode.equals(that.replacementMode) && Objects.equals(offerToken, that.offerToken) && Objects.equals(accountId, that.accountId) @@ -1188,7 +1174,6 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash( product, - prorationMode, replacementMode, offerToken, accountId, @@ -1207,14 +1192,6 @@ public static final class Builder { return this; } - private @Nullable Long prorationMode; - - @CanIgnoreReturnValue - public @NonNull Builder setProrationMode(@NonNull Long setterArg) { - this.prorationMode = setterArg; - return this; - } - private @Nullable Long replacementMode; @CanIgnoreReturnValue @@ -1266,7 +1243,6 @@ public static final class Builder { public @NonNull PlatformBillingFlowParams build() { PlatformBillingFlowParams pigeonReturn = new PlatformBillingFlowParams(); pigeonReturn.setProduct(product); - pigeonReturn.setProrationMode(prorationMode); pigeonReturn.setReplacementMode(replacementMode); pigeonReturn.setOfferToken(offerToken); pigeonReturn.setAccountId(accountId); @@ -1279,9 +1255,8 @@ public static final class Builder { @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList<>(8); + ArrayList toListResult = new ArrayList<>(7); toListResult.add(product); - toListResult.add(prorationMode); toListResult.add(replacementMode); toListResult.add(offerToken); toListResult.add(accountId); @@ -1295,19 +1270,17 @@ ArrayList toList() { PlatformBillingFlowParams pigeonResult = new PlatformBillingFlowParams(); Object product = pigeonVar_list.get(0); pigeonResult.setProduct((String) product); - Object prorationMode = pigeonVar_list.get(1); - pigeonResult.setProrationMode((Long) prorationMode); - Object replacementMode = pigeonVar_list.get(2); + Object replacementMode = pigeonVar_list.get(1); pigeonResult.setReplacementMode((Long) replacementMode); - Object offerToken = pigeonVar_list.get(3); + Object offerToken = pigeonVar_list.get(2); pigeonResult.setOfferToken((String) offerToken); - Object accountId = pigeonVar_list.get(4); + Object accountId = pigeonVar_list.get(3); pigeonResult.setAccountId((String) accountId); - Object obfuscatedProfileId = pigeonVar_list.get(5); + Object obfuscatedProfileId = pigeonVar_list.get(4); pigeonResult.setObfuscatedProfileId((String) obfuscatedProfileId); - Object oldProduct = pigeonVar_list.get(6); + Object oldProduct = pigeonVar_list.get(5); pigeonResult.setOldProduct((String) oldProduct); - Object purchaseToken = pigeonVar_list.get(7); + Object purchaseToken = pigeonVar_list.get(6); pigeonResult.setPurchaseToken((String) purchaseToken); return pigeonResult; } @@ -1691,6 +1664,16 @@ public void setAccountIdentifiers(@Nullable PlatformAccountIdentifiers setterArg this.accountIdentifiers = setterArg; } + private @Nullable PlatformPendingPurchaseUpdate pendingPurchaseUpdate; + + public @Nullable PlatformPendingPurchaseUpdate getPendingPurchaseUpdate() { + return pendingPurchaseUpdate; + } + + public void setPendingPurchaseUpdate(@Nullable PlatformPendingPurchaseUpdate setterArg) { + this.pendingPurchaseUpdate = setterArg; + } + /** Constructor is non-public to enforce null safety; use Builder. */ PlatformPurchase() {} @@ -1715,7 +1698,8 @@ public boolean equals(Object o) { && isAcknowledged.equals(that.isAcknowledged) && quantity.equals(that.quantity) && purchaseState.equals(that.purchaseState) - && Objects.equals(accountIdentifiers, that.accountIdentifiers); + && Objects.equals(accountIdentifiers, that.accountIdentifiers) + && Objects.equals(pendingPurchaseUpdate, that.pendingPurchaseUpdate); } @Override @@ -1733,7 +1717,8 @@ public int hashCode() { isAcknowledged, quantity, purchaseState, - accountIdentifiers); + accountIdentifiers, + pendingPurchaseUpdate); } public static final class Builder { @@ -1843,6 +1828,15 @@ public static final class Builder { return this; } + private @Nullable PlatformPendingPurchaseUpdate pendingPurchaseUpdate; + + @CanIgnoreReturnValue + public @NonNull Builder setPendingPurchaseUpdate( + @Nullable PlatformPendingPurchaseUpdate setterArg) { + this.pendingPurchaseUpdate = setterArg; + return this; + } + public @NonNull PlatformPurchase build() { PlatformPurchase pigeonReturn = new PlatformPurchase(); pigeonReturn.setOrderId(orderId); @@ -1858,13 +1852,14 @@ public static final class Builder { pigeonReturn.setQuantity(quantity); pigeonReturn.setPurchaseState(purchaseState); pigeonReturn.setAccountIdentifiers(accountIdentifiers); + pigeonReturn.setPendingPurchaseUpdate(pendingPurchaseUpdate); return pigeonReturn; } } @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList<>(13); + ArrayList toListResult = new ArrayList<>(14); toListResult.add(orderId); toListResult.add(packageName); toListResult.add(purchaseTime); @@ -1878,6 +1873,7 @@ ArrayList toList() { toListResult.add(quantity); toListResult.add(purchaseState); toListResult.add(accountIdentifiers); + toListResult.add(pendingPurchaseUpdate); return toListResult; } @@ -1909,6 +1905,107 @@ ArrayList toList() { pigeonResult.setPurchaseState((PlatformPurchaseState) purchaseState); Object accountIdentifiers = pigeonVar_list.get(12); pigeonResult.setAccountIdentifiers((PlatformAccountIdentifiers) accountIdentifiers); + Object pendingPurchaseUpdate = pigeonVar_list.get(13); + pigeonResult.setPendingPurchaseUpdate((PlatformPendingPurchaseUpdate) pendingPurchaseUpdate); + return pigeonResult; + } + } + + /** + * Pigeon version of Java Purchase. + * + *

See also PendingPurchaseUpdateWrapper on the Dart side. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PlatformPendingPurchaseUpdate { + private @NonNull List products; + + public @NonNull List getProducts() { + return products; + } + + public void setProducts(@NonNull List setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"products\" is null."); + } + this.products = setterArg; + } + + private @NonNull String purchaseToken; + + public @NonNull String getPurchaseToken() { + return purchaseToken; + } + + public void setPurchaseToken(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"purchaseToken\" is null."); + } + this.purchaseToken = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PlatformPendingPurchaseUpdate() {} + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PlatformPendingPurchaseUpdate that = (PlatformPendingPurchaseUpdate) o; + return products.equals(that.products) && purchaseToken.equals(that.purchaseToken); + } + + @Override + public int hashCode() { + return Objects.hash(products, purchaseToken); + } + + public static final class Builder { + + private @Nullable List products; + + @CanIgnoreReturnValue + public @NonNull Builder setProducts(@NonNull List setterArg) { + this.products = setterArg; + return this; + } + + private @Nullable String purchaseToken; + + @CanIgnoreReturnValue + public @NonNull Builder setPurchaseToken(@NonNull String setterArg) { + this.purchaseToken = setterArg; + return this; + } + + public @NonNull PlatformPendingPurchaseUpdate build() { + PlatformPendingPurchaseUpdate pigeonReturn = new PlatformPendingPurchaseUpdate(); + pigeonReturn.setProducts(products); + pigeonReturn.setPurchaseToken(purchaseToken); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(2); + toListResult.add(products); + toListResult.add(purchaseToken); + return toListResult; + } + + static @NonNull PlatformPendingPurchaseUpdate fromList( + @NonNull ArrayList pigeonVar_list) { + PlatformPendingPurchaseUpdate pigeonResult = new PlatformPendingPurchaseUpdate(); + Object products = pigeonVar_list.get(0); + pigeonResult.setProducts((List) products); + Object purchaseToken = pigeonVar_list.get(1); + pigeonResult.setPurchaseToken((String) purchaseToken); return pigeonResult; } } @@ -2410,6 +2507,16 @@ public void setPricingPhases(@NonNull List setterArg) { this.pricingPhases = setterArg; } + private @Nullable PlatformInstallmentPlanDetails installmentPlanDetails; + + public @Nullable PlatformInstallmentPlanDetails getInstallmentPlanDetails() { + return installmentPlanDetails; + } + + public void setInstallmentPlanDetails(@Nullable PlatformInstallmentPlanDetails setterArg) { + this.installmentPlanDetails = setterArg; + } + /** Constructor is non-public to enforce null safety; use Builder. */ PlatformSubscriptionOfferDetails() {} @@ -2426,12 +2533,14 @@ public boolean equals(Object o) { && Objects.equals(offerId, that.offerId) && offerToken.equals(that.offerToken) && offerTags.equals(that.offerTags) - && pricingPhases.equals(that.pricingPhases); + && pricingPhases.equals(that.pricingPhases) + && Objects.equals(installmentPlanDetails, that.installmentPlanDetails); } @Override public int hashCode() { - return Objects.hash(basePlanId, offerId, offerToken, offerTags, pricingPhases); + return Objects.hash( + basePlanId, offerId, offerToken, offerTags, pricingPhases, installmentPlanDetails); } public static final class Builder { @@ -2476,6 +2585,15 @@ public static final class Builder { return this; } + private @Nullable PlatformInstallmentPlanDetails installmentPlanDetails; + + @CanIgnoreReturnValue + public @NonNull Builder setInstallmentPlanDetails( + @Nullable PlatformInstallmentPlanDetails setterArg) { + this.installmentPlanDetails = setterArg; + return this; + } + public @NonNull PlatformSubscriptionOfferDetails build() { PlatformSubscriptionOfferDetails pigeonReturn = new PlatformSubscriptionOfferDetails(); pigeonReturn.setBasePlanId(basePlanId); @@ -2483,18 +2601,20 @@ public static final class Builder { pigeonReturn.setOfferToken(offerToken); pigeonReturn.setOfferTags(offerTags); pigeonReturn.setPricingPhases(pricingPhases); + pigeonReturn.setInstallmentPlanDetails(installmentPlanDetails); return pigeonReturn; } } @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList<>(5); + ArrayList toListResult = new ArrayList<>(6); toListResult.add(basePlanId); toListResult.add(offerId); toListResult.add(offerToken); toListResult.add(offerTags); toListResult.add(pricingPhases); + toListResult.add(installmentPlanDetails); return toListResult; } @@ -2511,6 +2631,9 @@ ArrayList toList() { pigeonResult.setOfferTags((List) offerTags); Object pricingPhases = pigeonVar_list.get(4); pigeonResult.setPricingPhases((List) pricingPhases); + Object installmentPlanDetails = pigeonVar_list.get(5); + pigeonResult.setInstallmentPlanDetails( + (PlatformInstallmentPlanDetails) installmentPlanDetails); return pigeonResult; } } @@ -2755,6 +2878,177 @@ ArrayList toList() { } } + /** + * Pigeon version of ProductDetails.InstallmentPlanDetails. + * https://developer.android.com/reference/com/android/billingclient/api/PendingPurchasesParams.Builder#enableOneTimeProducts() + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PlatformInstallmentPlanDetails { + private @NonNull Long commitmentPaymentsCount; + + public @NonNull Long getCommitmentPaymentsCount() { + return commitmentPaymentsCount; + } + + public void setCommitmentPaymentsCount(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"commitmentPaymentsCount\" is null."); + } + this.commitmentPaymentsCount = setterArg; + } + + private @NonNull Long subsequentCommitmentPaymentsCount; + + public @NonNull Long getSubsequentCommitmentPaymentsCount() { + return subsequentCommitmentPaymentsCount; + } + + public void setSubsequentCommitmentPaymentsCount(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException( + "Nonnull field \"subsequentCommitmentPaymentsCount\" is null."); + } + this.subsequentCommitmentPaymentsCount = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PlatformInstallmentPlanDetails() {} + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PlatformInstallmentPlanDetails that = (PlatformInstallmentPlanDetails) o; + return commitmentPaymentsCount.equals(that.commitmentPaymentsCount) + && subsequentCommitmentPaymentsCount.equals(that.subsequentCommitmentPaymentsCount); + } + + @Override + public int hashCode() { + return Objects.hash(commitmentPaymentsCount, subsequentCommitmentPaymentsCount); + } + + public static final class Builder { + + private @Nullable Long commitmentPaymentsCount; + + @CanIgnoreReturnValue + public @NonNull Builder setCommitmentPaymentsCount(@NonNull Long setterArg) { + this.commitmentPaymentsCount = setterArg; + return this; + } + + private @Nullable Long subsequentCommitmentPaymentsCount; + + @CanIgnoreReturnValue + public @NonNull Builder setSubsequentCommitmentPaymentsCount(@NonNull Long setterArg) { + this.subsequentCommitmentPaymentsCount = setterArg; + return this; + } + + public @NonNull PlatformInstallmentPlanDetails build() { + PlatformInstallmentPlanDetails pigeonReturn = new PlatformInstallmentPlanDetails(); + pigeonReturn.setCommitmentPaymentsCount(commitmentPaymentsCount); + pigeonReturn.setSubsequentCommitmentPaymentsCount(subsequentCommitmentPaymentsCount); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(2); + toListResult.add(commitmentPaymentsCount); + toListResult.add(subsequentCommitmentPaymentsCount); + return toListResult; + } + + static @NonNull PlatformInstallmentPlanDetails fromList( + @NonNull ArrayList pigeonVar_list) { + PlatformInstallmentPlanDetails pigeonResult = new PlatformInstallmentPlanDetails(); + Object commitmentPaymentsCount = pigeonVar_list.get(0); + pigeonResult.setCommitmentPaymentsCount((Long) commitmentPaymentsCount); + Object subsequentCommitmentPaymentsCount = pigeonVar_list.get(1); + pigeonResult.setSubsequentCommitmentPaymentsCount((Long) subsequentCommitmentPaymentsCount); + return pigeonResult; + } + } + + /** + * Pigeon version of Java PendingPurchasesParams. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PendingPurchasesParams { + private @NonNull Boolean enablePrepaidPlans; + + public @NonNull Boolean getEnablePrepaidPlans() { + return enablePrepaidPlans; + } + + public void setEnablePrepaidPlans(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"enablePrepaidPlans\" is null."); + } + this.enablePrepaidPlans = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PendingPurchasesParams() {} + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PendingPurchasesParams that = (PendingPurchasesParams) o; + return enablePrepaidPlans.equals(that.enablePrepaidPlans); + } + + @Override + public int hashCode() { + return Objects.hash(enablePrepaidPlans); + } + + public static final class Builder { + + private @Nullable Boolean enablePrepaidPlans; + + @CanIgnoreReturnValue + public @NonNull Builder setEnablePrepaidPlans(@NonNull Boolean setterArg) { + this.enablePrepaidPlans = setterArg; + return this; + } + + public @NonNull PendingPurchasesParams build() { + PendingPurchasesParams pigeonReturn = new PendingPurchasesParams(); + pigeonReturn.setEnablePrepaidPlans(enablePrepaidPlans); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(1); + toListResult.add(enablePrepaidPlans); + return toListResult; + } + + static @NonNull PendingPurchasesParams fromList(@NonNull ArrayList pigeonVar_list) { + PendingPurchasesParams pigeonResult = new PendingPurchasesParams(); + Object enablePrepaidPlans = pigeonVar_list.get(0); + pigeonResult.setEnablePrepaidPlans((Boolean) enablePrepaidPlans); + return pigeonResult; + } + } + private static class PigeonCodec extends StandardMessageCodec { public static final PigeonCodec INSTANCE = new PigeonCodec(); @@ -2812,17 +3106,23 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { case (byte) 143: return PlatformPurchase.fromList((ArrayList) readValue(buffer)); case (byte) 144: - return PlatformPurchaseHistoryRecord.fromList((ArrayList) readValue(buffer)); + return PlatformPendingPurchaseUpdate.fromList((ArrayList) readValue(buffer)); case (byte) 145: - return PlatformPurchaseHistoryResponse.fromList((ArrayList) readValue(buffer)); + return PlatformPurchaseHistoryRecord.fromList((ArrayList) readValue(buffer)); case (byte) 146: - return PlatformPurchasesResponse.fromList((ArrayList) readValue(buffer)); + return PlatformPurchaseHistoryResponse.fromList((ArrayList) readValue(buffer)); case (byte) 147: - return PlatformSubscriptionOfferDetails.fromList((ArrayList) readValue(buffer)); + return PlatformPurchasesResponse.fromList((ArrayList) readValue(buffer)); case (byte) 148: - return PlatformUserChoiceDetails.fromList((ArrayList) readValue(buffer)); + return PlatformSubscriptionOfferDetails.fromList((ArrayList) readValue(buffer)); case (byte) 149: + return PlatformUserChoiceDetails.fromList((ArrayList) readValue(buffer)); + case (byte) 150: return PlatformUserChoiceProduct.fromList((ArrayList) readValue(buffer)); + case (byte) 151: + return PlatformInstallmentPlanDetails.fromList((ArrayList) readValue(buffer)); + case (byte) 152: + return PendingPurchasesParams.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); } @@ -2876,24 +3176,33 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { } else if (value instanceof PlatformPurchase) { stream.write(143); writeValue(stream, ((PlatformPurchase) value).toList()); - } else if (value instanceof PlatformPurchaseHistoryRecord) { + } else if (value instanceof PlatformPendingPurchaseUpdate) { stream.write(144); + writeValue(stream, ((PlatformPendingPurchaseUpdate) value).toList()); + } else if (value instanceof PlatformPurchaseHistoryRecord) { + stream.write(145); writeValue(stream, ((PlatformPurchaseHistoryRecord) value).toList()); } else if (value instanceof PlatformPurchaseHistoryResponse) { - stream.write(145); + stream.write(146); writeValue(stream, ((PlatformPurchaseHistoryResponse) value).toList()); } else if (value instanceof PlatformPurchasesResponse) { - stream.write(146); + stream.write(147); writeValue(stream, ((PlatformPurchasesResponse) value).toList()); } else if (value instanceof PlatformSubscriptionOfferDetails) { - stream.write(147); + stream.write(148); writeValue(stream, ((PlatformSubscriptionOfferDetails) value).toList()); } else if (value instanceof PlatformUserChoiceDetails) { - stream.write(148); + stream.write(149); writeValue(stream, ((PlatformUserChoiceDetails) value).toList()); } else if (value instanceof PlatformUserChoiceProduct) { - stream.write(149); + stream.write(150); writeValue(stream, ((PlatformUserChoiceProduct) value).toList()); + } else if (value instanceof PlatformInstallmentPlanDetails) { + stream.write(151); + writeValue(stream, ((PlatformInstallmentPlanDetails) value).toList()); + } else if (value instanceof PendingPurchasesParams) { + stream.write(152); + writeValue(stream, ((PendingPurchasesParams) value).toList()); } else { super.writeValue(stream, value); } @@ -2933,6 +3242,7 @@ public interface InAppPurchaseApi { void startConnection( @NonNull Long callbackHandle, @NonNull PlatformBillingChoiceMode billingMode, + @NonNull PendingPurchasesParams pendingPurchasesParams, @NonNull Result result); /** Wraps BillingClient#endConnection(BillingClientStateListener). */ void endConnection(); @@ -3037,6 +3347,8 @@ static void setUp( ArrayList args = (ArrayList) message; Long callbackHandleArg = (Long) args.get(0); PlatformBillingChoiceMode billingModeArg = (PlatformBillingChoiceMode) args.get(1); + PendingPurchasesParams pendingPurchasesParamsArg = + (PendingPurchasesParams) args.get(2); Result resultCallback = new Result() { public void success(PlatformBillingResult result) { @@ -3050,7 +3362,8 @@ public void error(Throwable error) { } }; - api.startConnection(callbackHandleArg, billingModeArg, resultCallback); + api.startConnection( + callbackHandleArg, billingModeArg, pendingPurchasesParamsArg, resultCallback); }); } else { channel.setMessageHandler(null); 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 c7305a699382..9cc8b370ac20 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 @@ -51,15 +51,6 @@ /** Handles method channel for the plugin. */ class MethodCallHandlerImpl implements Application.ActivityLifecycleCallbacks, InAppPurchaseApi { - // TODO(gmackall): Replace uses of deprecated ProrationMode enum values with new - // ReplacementMode enum values. - // https://github.com/flutter/flutter/issues/128957. - @SuppressWarnings(value = "deprecation") - @VisibleForTesting - static final int PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY = - com.android.billingclient.api.BillingFlowParams.ProrationMode - .UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY; - @VisibleForTesting static final int REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY = com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams.ReplacementMode @@ -290,23 +281,12 @@ public void queryProductDetailsAsync( } } - if (params.getProrationMode() != PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY - && params.getReplacementMode() - != REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY) { - throw new FlutterError( - "IN_APP_PURCHASE_CONFLICT_PRORATION_MODE_REPLACEMENT_MODE", - "launchBillingFlow failed because you provided both prorationMode and replacementMode. You can only provide one of them.", - null); - } - if (params.getOldProduct() == null - && (params.getProrationMode() - != PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY - || params.getReplacementMode() - != REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY)) { + && (params.getReplacementMode() + != REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY)) { throw new FlutterError( "IN_APP_PURCHASE_REQUIRE_OLD_PRODUCT", - "launchBillingFlow failed because oldProduct is null. You must provide a valid oldProduct in order to use a proration mode.", + "launchBillingFlow failed because oldProduct is null. You must provide a valid oldProduct in order to use a replacement mode.", null); } else if (params.getOldProduct() != null && !cachedProducts.containsKey(params.getOldProduct())) { @@ -352,11 +332,6 @@ public void queryProductDetailsAsync( && !params.getOldProduct().isEmpty() && params.getPurchaseToken() != null) { subscriptionUpdateParamsBuilder.setOldPurchaseToken(params.getPurchaseToken()); - if (params.getProrationMode() - != PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY) { - setReplaceProrationMode( - subscriptionUpdateParamsBuilder, params.getProrationMode().intValue()); - } if (params.getReplacementMode() != REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY) { subscriptionUpdateParamsBuilder.setSubscriptionReplacementMode( @@ -367,16 +342,6 @@ public void queryProductDetailsAsync( return fromBillingResult(billingClient.launchBillingFlow(activity, paramsBuilder.build())); } - // TODO(gmackall): Replace uses of deprecated setReplaceProrationMode. - // https://github.com/flutter/flutter/issues/128957. - @SuppressWarnings(value = "deprecation") - private void setReplaceProrationMode( - BillingFlowParams.SubscriptionUpdateParams.Builder builder, int prorationMode) { - // The proration mode value has to match one of the following declared in - // https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode - builder.setReplaceProrationMode(prorationMode); - } - @Override public void consumeAsync( @NonNull String purchaseToken, @NonNull Result result) { @@ -428,6 +393,7 @@ public void queryPurchasesAsync( } @Override + @Deprecated public void queryPurchaseHistoryAsync( @NonNull PlatformProductType productType, @NonNull Result result) { @@ -457,10 +423,12 @@ public void queryPurchaseHistoryAsync( public void startConnection( @NonNull Long handle, @NonNull PlatformBillingChoiceMode billingMode, + @NonNull Messages.PendingPurchasesParams pendingPurchasesParams, @NonNull Result result) { if (billingClient == null) { billingClient = - billingClientFactory.createBillingClient(applicationContext, callbackApi, billingMode); + billingClientFactory.createBillingClient( + applicationContext, callbackApi, billingMode, pendingPurchasesParams); } try { 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 c06b3acfb503..0b71a08f2f52 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 @@ -22,6 +22,7 @@ import io.flutter.plugins.inapppurchase.Messages.PlatformBillingConfigResponse; import io.flutter.plugins.inapppurchase.Messages.PlatformBillingResult; import io.flutter.plugins.inapppurchase.Messages.PlatformOneTimePurchaseOfferDetails; +import io.flutter.plugins.inapppurchase.Messages.PlatformPendingPurchaseUpdate; import io.flutter.plugins.inapppurchase.Messages.PlatformPricingPhase; import io.flutter.plugins.inapppurchase.Messages.PlatformProductDetails; import io.flutter.plugins.inapppurchase.Messages.PlatformProductType; @@ -146,6 +147,8 @@ static PlatformProductType toPlatformProductType(@NonNull String typeString) { .setOfferTags(subscriptionOfferDetails.getOfferTags()) .setOfferToken(subscriptionOfferDetails.getOfferToken()) .setPricingPhases(fromPricingPhases(subscriptionOfferDetails.getPricingPhases())) + .setInstallmentPlanDetails( + fromInstallmentPlanDetails(subscriptionOfferDetails.getInstallmentPlanDetails())) .build(); } @@ -170,6 +173,20 @@ static PlatformProductType toPlatformProductType(@NonNull String typeString) { .build(); } + static @Nullable Messages.PlatformInstallmentPlanDetails fromInstallmentPlanDetails( + @Nullable ProductDetails.InstallmentPlanDetails installmentPlanDetails) { + if (installmentPlanDetails == null) { + return null; + } + + return new Messages.PlatformInstallmentPlanDetails.Builder() + .setCommitmentPaymentsCount( + (long) installmentPlanDetails.getInstallmentPlanCommitmentPaymentsCount()) + .setSubsequentCommitmentPaymentsCount( + (long) installmentPlanDetails.getSubsequentInstallmentPlanCommitmentPaymentsCount()) + .build(); + } + static PlatformRecurrenceMode toPlatformRecurrenceMode(int mode) { switch (mode) { case ProductDetails.RecurrenceMode.FINITE_RECURRING: @@ -217,9 +234,27 @@ static PlatformPurchaseState toPlatformPurchaseState(int state) { .setObfuscatedProfileId(accountIdentifiers.getObfuscatedProfileId()) .build()); } + + Purchase.PendingPurchaseUpdate pendingPurchaseUpdate = purchase.getPendingPurchaseUpdate(); + if (pendingPurchaseUpdate != null) { + builder.setPendingPurchaseUpdate(fromPendingPurchaseUpdate(pendingPurchaseUpdate)); + } + return builder.build(); } + static @Nullable PlatformPendingPurchaseUpdate fromPendingPurchaseUpdate( + @Nullable Purchase.PendingPurchaseUpdate pendingPurchaseUpdate) { + if (pendingPurchaseUpdate == null) { + return null; + } + + return new Messages.PlatformPendingPurchaseUpdate.Builder() + .setPurchaseToken(pendingPurchaseUpdate.getPurchaseToken()) + .setProducts(pendingPurchaseUpdate.getProducts()) + .build(); + } + static @NonNull PlatformPurchaseHistoryRecord fromPurchaseHistoryRecord( @NonNull PurchaseHistoryRecord purchaseHistoryRecord) { return new PlatformPurchaseHistoryRecord.Builder() diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImplTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImplTest.java index d1c91fb8219c..50265bab2315 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImplTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImplTest.java @@ -55,7 +55,7 @@ public void playBillingOnly() { // No logic to verify just ensure creation works. BillingClient client = factory.createBillingClient( - context, mockCallbackApi, PlatformBillingChoiceMode.PLAY_BILLING_ONLY); + context, mockCallbackApi, PlatformBillingChoiceMode.PLAY_BILLING_ONLY, null); assertNotNull(client); } @@ -64,7 +64,7 @@ public void alternativeBillingOnly() { // No logic to verify just ensure creation works. BillingClient client = factory.createBillingClient( - context, mockCallbackApi, PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY); + context, mockCallbackApi, PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY, null); assertNotNull(client); } @@ -78,7 +78,7 @@ public void userChoiceBilling() { final BillingClient billingClient = factory.createBillingClient( - context, mockCallbackApi, PlatformBillingChoiceMode.USER_CHOICE_BILLING); + context, mockCallbackApi, PlatformBillingChoiceMode.USER_CHOICE_BILLING, null); UserChoiceDetails details = mock(UserChoiceDetails.class); final String externalTransactionToken = "someLongTokenId1234"; @@ -98,6 +98,17 @@ public void userChoiceBilling() { assertTrue(callbackCaptor.getValue().getProducts().isEmpty()); } + @Test + public void pendingPurchasesForPrepaidPlans() { + // No logic to verify just ensure creation works. + Messages.PendingPurchasesParams params = + new Messages.PendingPurchasesParams.Builder().setEnablePrepaidPlans(true).build(); + BillingClient client = + factory.createBillingClient( + context, mockCallbackApi, PlatformBillingChoiceMode.PLAY_BILLING_ONLY, params); + assertNotNull(client); + } + @After public void tearDown() throws Exception { openMocks.close(); 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 58b1c42a0e41..597425a957a5 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 @@ -5,7 +5,6 @@ package io.flutter.plugins.inapppurchase; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.ACTIVITY_UNAVAILABLE; -import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY; import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; @@ -104,22 +103,33 @@ public class MethodCallHandlerTest { @Mock Context context; @Mock ActivityPluginBinding mockActivityPluginBinding; + private final Messages.PendingPurchasesParams defaultPendingPurchasesParams = + new Messages.PendingPurchasesParams.Builder().setEnablePrepaidPlans(false).build(); + private final Long DEFAULT_HANDLE = 1L; @Before public void setUp() { openMocks = MockitoAnnotations.openMocks(this); + // Use the same client no matter if alternative billing is enabled or not. when(factory.createBillingClient( - context, mockCallbackApi, PlatformBillingChoiceMode.PLAY_BILLING_ONLY)) + context, + mockCallbackApi, + PlatformBillingChoiceMode.PLAY_BILLING_ONLY, + defaultPendingPurchasesParams)) .thenReturn(mockBillingClient); when(factory.createBillingClient( - context, mockCallbackApi, PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY)) + context, + mockCallbackApi, + PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY, + defaultPendingPurchasesParams)) .thenReturn(mockBillingClient); when(factory.createBillingClient( any(Context.class), any(InAppPurchaseCallbackApi.class), - eq(PlatformBillingChoiceMode.USER_CHOICE_BILLING))) + eq(PlatformBillingChoiceMode.USER_CHOICE_BILLING), + any(Messages.PendingPurchasesParams.class))) .thenReturn(mockBillingClient); methodChannelHandler = new MethodCallHandlerImpl(activity, context, mockCallbackApi, factory); when(mockActivityPluginBinding.getActivity()).thenReturn(activity); @@ -159,10 +169,15 @@ public void isReady_clientDisconnected() { @Test public void startConnection() { ArgumentCaptor captor = - mockStartConnection(PlatformBillingChoiceMode.PLAY_BILLING_ONLY); + mockStartConnection( + PlatformBillingChoiceMode.PLAY_BILLING_ONLY, defaultPendingPurchasesParams); verify(platformBillingResult, never()).success(any()); verify(factory, times(1)) - .createBillingClient(context, mockCallbackApi, PlatformBillingChoiceMode.PLAY_BILLING_ONLY); + .createBillingClient( + context, + mockCallbackApi, + PlatformBillingChoiceMode.PLAY_BILLING_ONLY, + defaultPendingPurchasesParams); BillingResult billingResult = buildBillingResult(); captor.getValue().onBillingSetupFinished(billingResult); @@ -177,11 +192,15 @@ public void startConnection() { @Test public void startConnectionAlternativeBillingOnly() { ArgumentCaptor captor = - mockStartConnection(PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY); + mockStartConnection( + PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY, defaultPendingPurchasesParams); verify(platformBillingResult, never()).success(any()); verify(factory, times(1)) .createBillingClient( - context, mockCallbackApi, PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY); + context, + mockCallbackApi, + PlatformBillingChoiceMode.ALTERNATIVE_BILLING_ONLY, + defaultPendingPurchasesParams); BillingResult billingResult = buildBillingResult(); captor.getValue().onBillingSetupFinished(billingResult); @@ -193,17 +212,41 @@ public void startConnectionAlternativeBillingOnly() { verify(platformBillingResult, never()).error(any()); } + @Test + public void startConnectionPendingPurchasesPrepaidPlans() { + Messages.PendingPurchasesParams pendingPurchasesParams = + new Messages.PendingPurchasesParams.Builder().setEnablePrepaidPlans(true).build(); + ArgumentCaptor captor = + mockStartConnection(PlatformBillingChoiceMode.USER_CHOICE_BILLING, pendingPurchasesParams); + verify(platformBillingResult, never()).success(any()); + verify(factory, times(1)) + .createBillingClient( + context, + mockCallbackApi, + PlatformBillingChoiceMode.USER_CHOICE_BILLING, + pendingPurchasesParams); + + BillingResult billingResult = buildBillingResult(); + captor.getValue().onBillingSetupFinished(billingResult); + + ArgumentCaptor resultCaptor = + ArgumentCaptor.forClass(PlatformBillingResult.class); + verify(platformBillingResult, times(1)).success(resultCaptor.capture()); + } + @Test public void startConnectionUserChoiceBilling() { ArgumentCaptor captor = - mockStartConnection(PlatformBillingChoiceMode.USER_CHOICE_BILLING); + mockStartConnection( + PlatformBillingChoiceMode.USER_CHOICE_BILLING, defaultPendingPurchasesParams); verify(platformBillingResult, never()).success(any()); verify(factory, times(1)) .createBillingClient( any(Context.class), any(InAppPurchaseCallbackApi.class), - eq(PlatformBillingChoiceMode.USER_CHOICE_BILLING)); + eq(PlatformBillingChoiceMode.USER_CHOICE_BILLING), + any(Messages.PendingPurchasesParams.class)); BillingResult billingResult = BillingResult.newBuilder() @@ -221,10 +264,15 @@ public void startConnectionUserChoiceBilling() { public void userChoiceBillingOnSecondConnection() { // First connection. ArgumentCaptor captor1 = - mockStartConnection(PlatformBillingChoiceMode.PLAY_BILLING_ONLY); + mockStartConnection( + PlatformBillingChoiceMode.PLAY_BILLING_ONLY, defaultPendingPurchasesParams); verify(platformBillingResult, never()).success(any()); verify(factory, times(1)) - .createBillingClient(context, mockCallbackApi, PlatformBillingChoiceMode.PLAY_BILLING_ONLY); + .createBillingClient( + context, + mockCallbackApi, + PlatformBillingChoiceMode.PLAY_BILLING_ONLY, + defaultPendingPurchasesParams); BillingResult billingResult1 = BillingResult.newBuilder() @@ -248,13 +296,15 @@ public void userChoiceBillingOnSecondConnection() { // Second connection. ArgumentCaptor captor2 = - mockStartConnection(PlatformBillingChoiceMode.USER_CHOICE_BILLING); + mockStartConnection( + PlatformBillingChoiceMode.USER_CHOICE_BILLING, defaultPendingPurchasesParams); verify(platformBillingResult, never()).success(any()); verify(factory, times(1)) .createBillingClient( any(Context.class), any(InAppPurchaseCallbackApi.class), - eq(PlatformBillingChoiceMode.USER_CHOICE_BILLING)); + eq(PlatformBillingChoiceMode.USER_CHOICE_BILLING), + eq(defaultPendingPurchasesParams)); BillingResult billingResult2 = BillingResult.newBuilder() @@ -273,7 +323,10 @@ public void startConnection_multipleCalls() { doNothing().when(mockBillingClient).startConnection(captor.capture()); methodChannelHandler.startConnection( - DEFAULT_HANDLE, PlatformBillingChoiceMode.PLAY_BILLING_ONLY, platformBillingResult); + DEFAULT_HANDLE, + PlatformBillingChoiceMode.PLAY_BILLING_ONLY, + defaultPendingPurchasesParams, + platformBillingResult); verify(platformBillingResult, never()).success(any()); BillingResult billingResult1 = buildBillingResult(); BillingResult billingResult2 = @@ -480,7 +533,10 @@ public void endConnection() { @SuppressWarnings("unchecked") final Messages.Result mockResult = mock(Messages.Result.class); methodChannelHandler.startConnection( - disconnectCallbackHandle, PlatformBillingChoiceMode.PLAY_BILLING_ONLY, mockResult); + disconnectCallbackHandle, + PlatformBillingChoiceMode.PLAY_BILLING_ONLY, + defaultPendingPurchasesParams, + mockResult); final BillingClientStateListener stateListener = captor.getValue(); // Disconnect the connected client @@ -555,8 +611,6 @@ public void launchBillingFlow_null_AccountId_do_not_crash() { queryForProducts(singletonList(productId)); PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); paramsBuilder.setProduct(productId); - paramsBuilder.setProrationMode( - (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); paramsBuilder.setReplacementMode( (long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); @@ -582,8 +636,6 @@ public void launchBillingFlow_ok_null_OldProduct() { PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); paramsBuilder.setProduct(productId); paramsBuilder.setAccountId(accountId); - paramsBuilder.setProrationMode( - (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); paramsBuilder.setReplacementMode( (long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); @@ -613,8 +665,6 @@ public void launchBillingFlow_ok_null_Activity() { PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); paramsBuilder.setProduct(productId); paramsBuilder.setAccountId(accountId); - paramsBuilder.setProrationMode( - (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); paramsBuilder.setReplacementMode( (long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); @@ -638,8 +688,6 @@ public void launchBillingFlow_ok_oldProduct() { paramsBuilder.setProduct(productId); paramsBuilder.setAccountId(accountId); paramsBuilder.setOldProduct(oldProductId); - paramsBuilder.setProrationMode( - (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); paramsBuilder.setReplacementMode( (long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); @@ -667,8 +715,6 @@ public void launchBillingFlow_ok_AccountId() { PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); paramsBuilder.setProduct(productId); paramsBuilder.setAccountId(accountId); - paramsBuilder.setProrationMode( - (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); paramsBuilder.setReplacementMode( (long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); @@ -687,27 +733,22 @@ public void launchBillingFlow_ok_AccountId() { assertResultsMatch(platformResult, billingResult); } - // TODO(gmackall): Replace uses of deprecated ProrationMode enum values with new - // ReplacementMode enum values. - // https://github.com/flutter/flutter/issues/128957. @Test - @SuppressWarnings(value = "deprecation") public void launchBillingFlow_ok_Proration() { // Fetch the product details first and query the method call String productId = "foo"; String oldProductId = "oldFoo"; String purchaseToken = "purchaseTokenFoo"; String accountId = "account"; - int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE; + int replacementMode = + BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE; queryForProducts(unmodifiableList(asList(productId, oldProductId))); PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); paramsBuilder.setProduct(productId); paramsBuilder.setAccountId(accountId); paramsBuilder.setOldProduct(oldProductId); paramsBuilder.setPurchaseToken(purchaseToken); - paramsBuilder.setProrationMode((long) prorationMode); - paramsBuilder.setReplacementMode( - (long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); + paramsBuilder.setReplacementMode((long) replacementMode); // Launch the billing flow BillingResult billingResult = buildBillingResult(); @@ -724,25 +765,20 @@ public void launchBillingFlow_ok_Proration() { assertResultsMatch(platformResult, billingResult); } - // TODO(gmackall): Replace uses of deprecated ProrationMode enum values with new - // ReplacementMode enum values. - // https://github.com/flutter/flutter/issues/128957. @Test - @SuppressWarnings(value = "deprecation") public void launchBillingFlow_ok_Proration_with_null_OldProduct() { // Fetch the product details first and query the method call String productId = "foo"; String accountId = "account"; String queryOldProductId = "oldFoo"; - int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE; + int replacementMode = + BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE; queryForProducts(unmodifiableList(asList(productId, queryOldProductId))); PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); paramsBuilder.setProduct(productId); paramsBuilder.setAccountId(accountId); paramsBuilder.setOldProduct(null); - paramsBuilder.setProrationMode((long) prorationMode); - paramsBuilder.setReplacementMode( - (long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); + paramsBuilder.setReplacementMode((long) replacementMode); // Launch the billing flow BillingResult billingResult = buildBillingResult(); @@ -773,8 +809,6 @@ public void launchBillingFlow_ok_Replacement_with_null_OldProduct() { paramsBuilder.setProduct(productId); paramsBuilder.setAccountId(accountId); paramsBuilder.setOldProduct(null); - paramsBuilder.setProrationMode( - (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); paramsBuilder.setReplacementMode((long) replacementMode); // Launch the billing flow @@ -793,60 +827,21 @@ public void launchBillingFlow_ok_Replacement_with_null_OldProduct() { } @Test - @SuppressWarnings(value = "deprecation") - public void launchBillingFlow_ok_Proration_and_Replacement_conflict() { - // Fetch the product details first and query the method call - String productId = "foo"; - String accountId = "account"; - String queryOldProductId = "oldFoo"; - int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE; - int replacementMode = - BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE; - queryForProducts(unmodifiableList(asList(productId, queryOldProductId))); - PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); - paramsBuilder.setProduct(productId); - paramsBuilder.setAccountId(accountId); - paramsBuilder.setOldProduct(queryOldProductId); - paramsBuilder.setProrationMode((long) prorationMode); - paramsBuilder.setReplacementMode((long) replacementMode); - - // Launch the billing flow - BillingResult billingResult = buildBillingResult(); - when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); - - // Assert that the synchronous call throws an exception. - FlutterError exception = - assertThrows( - FlutterError.class, - () -> methodChannelHandler.launchBillingFlow(paramsBuilder.build())); - assertEquals("IN_APP_PURCHASE_CONFLICT_PRORATION_MODE_REPLACEMENT_MODE", exception.code); - assertTrue( - Objects.requireNonNull(exception.getMessage()) - .contains( - "launchBillingFlow failed because you provided both prorationMode and replacementMode. You can only provide one of them.")); - } - - // TODO(gmackall): Replace uses of deprecated ProrationMode enum values with new - // ReplacementMode enum values. - // https://github.com/flutter/flutter/issues/128957. - @Test - @SuppressWarnings(value = "deprecation") public void launchBillingFlow_ok_Full() { // Fetch the product details first and query the method call String productId = "foo"; String oldProductId = "oldFoo"; String purchaseToken = "purchaseTokenFoo"; String accountId = "account"; - int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE; + int replacementMode = + BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_FULL_PRICE; queryForProducts(unmodifiableList(asList(productId, oldProductId))); PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); paramsBuilder.setProduct(productId); paramsBuilder.setAccountId(accountId); paramsBuilder.setOldProduct(oldProductId); paramsBuilder.setPurchaseToken(purchaseToken); - paramsBuilder.setProrationMode((long) prorationMode); - paramsBuilder.setReplacementMode( - (long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); + paramsBuilder.setReplacementMode((long) replacementMode); // Launch the billing flow BillingResult billingResult = buildBillingResult(); @@ -872,8 +867,6 @@ public void launchBillingFlow_clientDisconnected() { PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); paramsBuilder.setProduct(productId); paramsBuilder.setAccountId(accountId); - paramsBuilder.setProrationMode( - (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); paramsBuilder.setReplacementMode( (long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); @@ -895,8 +888,6 @@ public void launchBillingFlow_productNotFound() { PlatformBillingFlowParams.Builder paramsBuilder = new PlatformBillingFlowParams.Builder(); paramsBuilder.setProduct(productId); paramsBuilder.setAccountId(accountId); - paramsBuilder.setProrationMode( - (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); paramsBuilder.setReplacementMode( (long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); @@ -921,8 +912,6 @@ public void launchBillingFlow_oldProductNotFound() { paramsBuilder.setProduct(productId); paramsBuilder.setAccountId(accountId); paramsBuilder.setOldProduct(oldProductId); - paramsBuilder.setProrationMode( - (long) PRORATION_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); paramsBuilder.setReplacementMode( (long) REPLACEMENT_MODE_UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY); @@ -988,6 +977,7 @@ public void queryPurchases_returns_success() { } @Test + @SuppressWarnings(value = "deprecation") public void queryPurchaseHistoryAsync() { // Set up an established billing client and all our mocked responses establishConnectedBillingClient(); @@ -1015,6 +1005,7 @@ public void queryPurchaseHistoryAsync() { } @Test + @SuppressWarnings(value = "deprecation") public void queryPurchaseHistoryAsync_clientDisconnected() { methodChannelHandler.endConnection(); @@ -1137,7 +1128,8 @@ public void isFutureSupported_false() { *

Defaults to play billing only which is the default. */ private ArgumentCaptor mockStartConnection() { - return mockStartConnection(PlatformBillingChoiceMode.PLAY_BILLING_ONLY); + return mockStartConnection( + PlatformBillingChoiceMode.PLAY_BILLING_ONLY, defaultPendingPurchasesParams); } /** @@ -1145,12 +1137,14 @@ private ArgumentCaptor mockStartConnection() { * Messages.Result)} with startup params. */ private ArgumentCaptor mockStartConnection( - PlatformBillingChoiceMode billingChoiceMode) { + PlatformBillingChoiceMode billingChoiceMode, + Messages.PendingPurchasesParams pendingPurchasesParams) { ArgumentCaptor captor = ArgumentCaptor.forClass(BillingClientStateListener.class); doNothing().when(mockBillingClient).startConnection(captor.capture()); - methodChannelHandler.startConnection(DEFAULT_HANDLE, billingChoiceMode, platformBillingResult); + methodChannelHandler.startConnection( + DEFAULT_HANDLE, billingChoiceMode, pendingPurchasesParams, platformBillingResult); return captor; } @@ -1158,7 +1152,10 @@ private void establishConnectedBillingClient() { @SuppressWarnings("unchecked") final Messages.Result mockResult = mock(Messages.Result.class); methodChannelHandler.startConnection( - DEFAULT_HANDLE, PlatformBillingChoiceMode.PLAY_BILLING_ONLY, mockResult); + DEFAULT_HANDLE, + PlatformBillingChoiceMode.PLAY_BILLING_ONLY, + defaultPendingPurchasesParams, + mockResult); } private void queryForProducts(List productIdList) { 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 fff2ef2ffbc1..5f8e83cf401f 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 @@ -242,6 +242,13 @@ private void assertSerialized( assertEquals(expected.getOfferTags(), serialized.getOfferTags()); assertEquals(expected.getOfferToken(), serialized.getOfferToken()); assertSerialized(expected.getPricingPhases(), serialized.getPricingPhases()); + + ProductDetails.InstallmentPlanDetails expectedInstallmentPlanDetails = expected.getInstallmentPlanDetails(); + Messages.PlatformInstallmentPlanDetails serializedInstallmentPlanDetails = serialized.getInstallmentPlanDetails(); + assertEquals(expectedInstallmentPlanDetails == null, serializedInstallmentPlanDetails == null); + if (expectedInstallmentPlanDetails != null && serializedInstallmentPlanDetails != null) { + assertSerialized(expectedInstallmentPlanDetails, serializedInstallmentPlanDetails); + } } private void assertSerialized( @@ -287,6 +294,17 @@ private void assertSerialized(Purchase expected, Messages.PlatformPurchase seria Objects.requireNonNull(serialized.getAccountIdentifiers()).getObfuscatedProfileId()); } + private void assertSerialized( + ProductDetails.InstallmentPlanDetails expected, + Messages.PlatformInstallmentPlanDetails serialized) { + assertEquals( + expected.getInstallmentPlanCommitmentPaymentsCount(), + serialized.getCommitmentPaymentsCount().intValue()); + assertEquals( + expected.getSubsequentInstallmentPlanCommitmentPaymentsCount(), + serialized.getSubsequentCommitmentPaymentsCount().intValue()); + } + private String productTypeFromPlatform(Messages.PlatformProductType type) { switch (type) { case INAPP: diff --git a/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart b/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart index 4773446cdeeb..2d5bfe86da12 100644 --- a/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart +++ b/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart @@ -424,8 +424,8 @@ class _MyAppState extends State<_MyApp> { changeSubscriptionParam: oldSubscription != null ? ChangeSubscriptionParam( oldPurchaseDetails: oldSubscription, - prorationMode: ProrationMode - .immediateWithTimeProration) + replacementMode: + ReplacementMode.withTimeProration) : null); if (productDetails.id == _kConsumableId) { _inAppPurchasePlatform.buyConsumable( diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart index 8e265dbdcee8..0c63080686a4 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'billing_client_wrapper.dart'; +import 'pending_purchases_params_wrapper.dart'; import 'purchase_wrapper.dart'; import 'user_choice_details_wrapper.dart'; @@ -44,6 +45,8 @@ class BillingClientManager { BillingClientManager( {@visibleForTesting BillingClientFactory? billingClientFactory}) : _billingChoiceMode = BillingChoiceMode.playBillingOnly, + _pendingPurchasesParams = + const PendingPurchasesParamsWrapper(enablePrepaidPlans: false), _billingClientFactory = billingClientFactory ?? _createBillingClient { _connect(); } @@ -85,6 +88,7 @@ class BillingClientManager { BillingChoiceMode _billingChoiceMode; final BillingClientFactory _billingClientFactory; + PendingPurchasesParamsWrapper _pendingPurchasesParams; bool _isConnecting = false; bool _isDisposed = false; @@ -161,9 +165,14 @@ class BillingClientManager { Future reconnectWithBillingChoiceMode( BillingChoiceMode billingChoiceMode) async { _billingChoiceMode = billingChoiceMode; - // Ends connection and triggers OnBillingServiceDisconnected, which causes reconnect. - await client.endConnection(); - await _connect(); + await _reconnect(); + } + + /// Ends connection to [BillingClient] and reconnects with [pendingPurchasesParams]. + Future reconnectWithPendingPurchasesParams( + PendingPurchasesParamsWrapper pendingPurchasesParams) async { + _pendingPurchasesParams = pendingPurchasesParams; + await _reconnect(); } // If disposed, does nothing. @@ -179,13 +188,21 @@ class BillingClientManager { _isConnecting = true; _readyFuture = Future.sync(() async { await client.startConnection( - onBillingServiceDisconnected: _connect, - billingChoiceMode: _billingChoiceMode); + onBillingServiceDisconnected: _connect, + billingChoiceMode: _billingChoiceMode, + pendingPurchasesParams: _pendingPurchasesParams, + ); _isConnecting = false; }); return _readyFuture; } + Future _reconnect() async { + // Ends connection and triggers OnBillingServiceDisconnected, which causes reconnect. + await client.endConnection(); + await _connect(); + } + void _onPurchasesUpdated(PurchasesResultWrapper event) { if (_isDisposed) { return; 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 1f46c3f1de20..3a2ed36bed23 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 @@ -11,6 +11,7 @@ import '../../billing_client_wrappers.dart'; import '../messages.g.dart'; import '../pigeon_converters.dart'; import 'billing_config_wrapper.dart'; +import 'pending_purchases_params_wrapper.dart'; part 'billing_client_wrapper.g.dart'; @@ -81,18 +82,6 @@ class BillingClient { return _hostApi.isReady(); } - /// Enable the [BillingClientWrapper] to handle pending purchases. - /// - /// **Deprecation warning:** it is no longer required to call - /// [enablePendingPurchases] when initializing your application. - @Deprecated( - 'The requirement to call `enablePendingPurchases()` has become obsolete ' - "since Google Play no longer accepts app submissions that don't support " - 'pending purchases.') - void enablePendingPurchases() { - // No-op, until it is time to completely remove this method from the API. - } - /// Calls /// [`BillingClient#startConnection(BillingClientStateListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#startconnection) /// to create and connect a `BillingClient` instance. @@ -103,14 +92,23 @@ class BillingClient { /// /// This triggers the creation of a new `BillingClient` instance in Java if /// one doesn't already exist. - Future startConnection( - {required OnBillingServiceDisconnected onBillingServiceDisconnected, - BillingChoiceMode billingChoiceMode = - BillingChoiceMode.playBillingOnly}) async { + Future startConnection({ + required OnBillingServiceDisconnected onBillingServiceDisconnected, + BillingChoiceMode billingChoiceMode = BillingChoiceMode.playBillingOnly, + PendingPurchasesParamsWrapper? pendingPurchasesParams, + }) async { hostCallbackHandler.disconnectCallbacks.add(onBillingServiceDisconnected); - return resultWrapperFromPlatform(await _hostApi.startConnection( + return resultWrapperFromPlatform( + await _hostApi.startConnection( hostCallbackHandler.disconnectCallbacks.length - 1, - platformBillingChoiceMode(billingChoiceMode))); + platformBillingChoiceMode(billingChoiceMode), + switch (pendingPurchasesParams) { + final PendingPurchasesParamsWrapper params => + pendingPurchasesParamsFromWrapper(params), + null => PendingPurchasesParams(enablePrepaidPlans: false) + }, + ), + ); } /// Calls @@ -181,7 +179,7 @@ class BillingClient { /// existing subscription. /// The [oldProduct](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.Builder#setOldPurchaseToken(java.lang.String)) and [purchaseToken] are the product id and purchase token that the user is upgrading or downgrading from. /// [purchaseToken] must not be `null` if [oldProduct] is not `null`. - /// The [prorationMode](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.Builder#setReplaceProrationMode(int)) is the mode of proration during subscription upgrade/downgrade. + /// The [replacementMode](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.Builder#setSubscriptionReplacementMode(int)) is the mode of replacement during subscription upgrade/downgrade. /// This value will only be effective if the `oldProduct` is also set. Future launchBillingFlow( {required String product, @@ -190,15 +188,12 @@ class BillingClient { String? obfuscatedProfileId, String? oldProduct, String? purchaseToken, - ProrationMode? prorationMode, ReplacementMode? replacementMode}) async { assert((oldProduct == null) == (purchaseToken == null), 'oldProduct and purchaseToken must both be set, or both be null.'); return resultWrapperFromPlatform( await _hostApi.launchBillingFlow(PlatformBillingFlowParams( product: product, - prorationMode: const ProrationModeConverter().toJson(prorationMode ?? - ProrationMode.unknownSubscriptionUpgradeDowngradePolicy), replacementMode: const ReplacementModeConverter() .toJson(replacementMode ?? ReplacementMode.unknownReplacementMode), offerToken: offerToken, @@ -253,6 +248,7 @@ class BillingClient { /// /// 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( @@ -553,78 +549,6 @@ class ProductTypeConverter implements JsonConverter { String toJson(ProductType object) => _$ProductTypeEnumMap[object]!; } -/// Enum representing the proration mode. -/// -/// When upgrading or downgrading a subscription, set this mode to provide details -/// about the proration that will be applied when the subscription changes. -/// -/// Wraps [`BillingFlowParams.ProrationMode`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode) -/// See the linked documentation for an explanation of the different constants. -@JsonEnum(alwaysCreate: true) -enum ProrationMode { -// WARNING: Changes to this class need to be reflected in our generated code. -// Run `flutter packages pub run build_runner watch` to rebuild and watch for -// further changes. - - /// Unknown upgrade or downgrade policy. - @JsonValue(0) - unknownSubscriptionUpgradeDowngradePolicy, - - /// Replacement takes effect immediately, and the remaining time will be prorated - /// and credited to the user. - /// - /// This is the current default behavior. - @JsonValue(1) - immediateWithTimeProration, - - /// Replacement takes effect immediately, and the billing cycle remains the same. - /// - /// The price for the remaining period will be charged. - /// This option is only available for subscription upgrade. - @JsonValue(2) - immediateAndChargeProratedPrice, - - /// Replacement takes effect immediately, and the new price will be charged on next - /// recurrence time. - /// - /// The billing cycle stays the same. - @JsonValue(3) - immediateWithoutProration, - - /// Replacement takes effect when the old plan expires, and the new price will - /// be charged at the same time. - @JsonValue(4) - deferred, - - /// Replacement takes effect immediately, and the user is charged full price - /// of new plan and is given a full billing cycle of subscription, plus - /// remaining prorated time from the old plan. - @JsonValue(5) - immediateAndChargeFullPrice, -} - -/// Serializer for [ProrationMode]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@ProrationModeConverter()`. -class ProrationModeConverter implements JsonConverter { - /// Default const constructor. - const ProrationModeConverter(); - - @override - @Deprecated('JSON serialization is not intended for public use, and will ' - 'be removed in a future version.') - ProrationMode fromJson(int? json) { - if (json == null) { - return ProrationMode.unknownSubscriptionUpgradeDowngradePolicy; - } - return $enumDecode(_$ProrationModeEnumMap, json); - } - - @override - int toJson(ProrationMode object) => _$ProrationModeEnumMap[object]!; -} - /// Enum representing the replacement mode. /// /// When upgrading or downgrading a subscription, set this mode to provide details diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.g.dart index eb0033193d95..ea2fb7e75a20 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.g.dart @@ -33,15 +33,6 @@ const _$ProductTypeEnumMap = { ProductType.subs: 'subs', }; -const _$ProrationModeEnumMap = { - ProrationMode.unknownSubscriptionUpgradeDowngradePolicy: 0, - ProrationMode.immediateWithTimeProration: 1, - ProrationMode.immediateAndChargeProratedPrice: 2, - ProrationMode.immediateWithoutProration: 3, - ProrationMode.deferred: 4, - ProrationMode.immediateAndChargeFullPrice: 5, -}; - const _$ReplacementModeEnumMap = { ReplacementMode.unknownReplacementMode: 0, ReplacementMode.withTimeProration: 1, diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/pending_purchases_params_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/pending_purchases_params_wrapper.dart new file mode 100644 index 000000000000..e97ccdfa8e44 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/pending_purchases_params_wrapper.dart @@ -0,0 +1,53 @@ +// 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. + +import 'package:flutter/foundation.dart'; +import 'package:json_annotation/json_annotation.dart'; + +// WARNING: Changes to `@JsonSerializable` classes need to be reflected in the +// below generated file. Run `flutter packages pub run build_runner watch` to +// rebuild and watch for further changes. +part 'pending_purchases_params_wrapper.g.dart'; + +/// Dart wrapper around [`com.android.billingclient.api.PendingPurchasesParams`](https://developer.android.com/reference/com/android/billingclient/api/PendingPurchasesParams). +/// +/// Represents the parameters to enable pending purchases. +@JsonSerializable() +@immutable +class PendingPurchasesParamsWrapper { + /// Creates a [PendingPurchasesParamsWrapper]. + const PendingPurchasesParamsWrapper({ + required this.enablePrepaidPlans, + }); + + /// Factory for creating a [PendingPurchasesParamsWrapper] from a [Map]. + @Deprecated('JSON serialization is not intended for public use, and will ' + 'be removed in a future version.') + factory PendingPurchasesParamsWrapper.fromJson(Map map) => + _$PendingPurchasesParamsWrapperFromJson(map); + + /// Enables pending purchase for prepaid plans. + /// + /// Handling pending purchases for prepaid plans is different from one-time products. + /// Your application will need to be updated to ensure subscription entitlements are + /// managed correctly with pending transactions. + /// To learn more see https://developer.android.com/google/play/billing/subscriptions#pending. + @JsonKey(defaultValue: false) + final bool enablePrepaidPlans; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + + return other is PendingPurchasesParamsWrapper && + other.enablePrepaidPlans == enablePrepaidPlans; + } + + @override + int get hashCode { + return enablePrepaidPlans.hashCode; + } +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/pending_purchases_params_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/pending_purchases_params_wrapper.g.dart new file mode 100644 index 000000000000..491b63f61e63 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/pending_purchases_params_wrapper.g.dart @@ -0,0 +1,13 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'pending_purchases_params_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PendingPurchasesParamsWrapper _$PendingPurchasesParamsWrapperFromJson( + Map json) => + PendingPurchasesParamsWrapper( + enablePrepaidPlans: json['enablePrepaidPlans'] as bool? ?? false, + ); diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart index 8d17d81c9a49..59926ec712f3 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart @@ -39,6 +39,7 @@ class PurchaseWrapper { required this.purchaseState, this.obfuscatedAccountId, this.obfuscatedProfileId, + this.pendingPurchaseUpdate, }); /// Factory for creating a [PurchaseWrapper] from a [Map] with the purchase details. @@ -65,7 +66,8 @@ class PurchaseWrapper { other.isAutoRenewing == isAutoRenewing && other.originalJson == originalJson && other.isAcknowledged == isAcknowledged && - other.purchaseState == purchaseState; + other.purchaseState == purchaseState && + other.pendingPurchaseUpdate == pendingPurchaseUpdate; } @override @@ -79,7 +81,8 @@ class PurchaseWrapper { isAutoRenewing, originalJson, isAcknowledged, - purchaseState); + purchaseState, + pendingPurchaseUpdate); /// The unique ID for this purchase. Corresponds to the Google Payments order /// ID. @@ -158,6 +161,58 @@ class PurchaseWrapper { /// directly calling [BillingClient.launchBillingFlow] and is not available /// on the generic [InAppPurchasePlatform]. final String? obfuscatedProfileId; + + /// The [PendingPurchaseUpdateWrapper] for an uncommitted transaction. + /// + /// A PendingPurchaseUpdate is normally generated from a pending transaction + /// upgrading/downgrading an existing subscription. + /// Returns null if this purchase does not have a pending transaction. + final PendingPurchaseUpdateWrapper? pendingPurchaseUpdate; +} + +@JsonSerializable() +@immutable + +/// Represents a pending change/update to the existing purchase. +class PendingPurchaseUpdateWrapper { + /// Creates a pending purchase wrapper update wrapper with the given purchase details. + const PendingPurchaseUpdateWrapper({ + required this.purchaseToken, + required this.products, + }); + + /// Factory for creating a [PendingPurchaseUpdateWrapper] from a [Map] with the purchase details. + @Deprecated('JSON serialization is not intended for public use, and will ' + 'be removed in a future version.') + factory PendingPurchaseUpdateWrapper.fromJson(Map map) => + _$PendingPurchaseUpdateWrapperFromJson(map); + + /// A token that uniquely identifies this pending transaction. + @JsonKey(defaultValue: '') + final String purchaseToken; + + /// The product IDs of this pending purchase update. + @JsonKey(defaultValue: []) + final List products; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is PendingPurchaseUpdateWrapper && + other.purchaseToken == purchaseToken && + listEquals(other.products, products); + } + + @override + int get hashCode => Object.hash( + purchaseToken, + products.hashCode, + ); } /// Data structure representing a purchase history record. 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 b97afce0f628..32c6d492f43d 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 @@ -24,6 +24,19 @@ PurchaseWrapper _$PurchaseWrapperFromJson(Map json) => PurchaseWrapper( .fromJson((json['purchaseState'] as num?)?.toInt()), obfuscatedAccountId: json['obfuscatedAccountId'] as String?, obfuscatedProfileId: json['obfuscatedProfileId'] as String?, + pendingPurchaseUpdate: json['pendingPurchaseUpdate'] == null + ? null + : PendingPurchaseUpdateWrapper.fromJson( + Map.from(json['pendingPurchaseUpdate'] as Map)), + ); + +PendingPurchaseUpdateWrapper _$PendingPurchaseUpdateWrapperFromJson(Map json) => + PendingPurchaseUpdateWrapper( + purchaseToken: json['purchaseToken'] as String? ?? '', + products: (json['products'] as List?) + ?.map((e) => e as String) + .toList() ?? + [], ); PurchaseHistoryRecordWrapper _$PurchaseHistoryRecordWrapperFromJson(Map json) => diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/subscription_offer_details_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/subscription_offer_details_wrapper.dart index 4b5642ed0431..ba3b1bd10776 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/subscription_offer_details_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/subscription_offer_details_wrapper.dart @@ -26,6 +26,7 @@ class SubscriptionOfferDetailsWrapper { required this.offerTags, required this.offerIdToken, required this.pricingPhases, + this.installmentPlanDetails, }); /// Factory for creating a [SubscriptionOfferDetailsWrapper] from a [Map] @@ -59,6 +60,10 @@ class SubscriptionOfferDetailsWrapper { @JsonKey(defaultValue: []) final List pricingPhases; + /// Represents additional details of an installment subscription plan. + @JsonKey(defaultValue: null) + final InstallmentPlanDetailsWrapper? installmentPlanDetails; + @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) { @@ -70,7 +75,8 @@ class SubscriptionOfferDetailsWrapper { other.offerId == offerId && listEquals(other.offerTags, offerTags) && other.offerIdToken == offerIdToken && - listEquals(other.pricingPhases, pricingPhases); + listEquals(other.pricingPhases, pricingPhases) && + installmentPlanDetails == other.installmentPlanDetails; } @override @@ -81,6 +87,7 @@ class SubscriptionOfferDetailsWrapper { offerTags.hashCode, offerIdToken.hashCode, pricingPhases.hashCode, + installmentPlanDetails.hashCode, ); } } @@ -158,3 +165,61 @@ class PricingPhaseWrapper { recurrenceMode, ); } + +/// Represents additional details of an installment subscription plan. +@JsonSerializable() +@immutable +class InstallmentPlanDetailsWrapper { + /// Creates a [InstallmentPlanDetailsWrapper]. + const InstallmentPlanDetailsWrapper({ + required this.commitmentPaymentsCount, + required this.subsequentCommitmentPaymentsCount, + }); + + /// Factory for creating a [InstallmentPlanDetailsWrapper] from a [Map] + /// with the plan details. + @Deprecated('JSON serialization is not intended for public use, and will ' + 'be removed in a future version.') + factory InstallmentPlanDetailsWrapper.fromJson(Map map) => + _$InstallmentPlanDetailsWrapperFromJson(map); + + /// Committed payments count after a user signs up for this subscription plan. + /// + /// For example, for a monthly subscription plan with commitmentPaymentsCount + /// as 12, users will be charged monthly for 12 month after initial signup. + /// User cancellation won't take effect until all 12 committed payments are finished. + @JsonKey(defaultValue: 0) + final int commitmentPaymentsCount; + + /// Subsequent committed payments count after this subscription plan renews. + /// + /// For example, for a monthly subscription plan with subsequentCommitmentPaymentsCount + /// as 12, when the subscription plan renews, users will be committed to another fresh + /// 12 monthly payments. + /// + /// Note: Returns 0 if the installment plan doesn't have any subsequent committment, + /// which means this subscription plan will fall back to a normal non-installment + /// monthly plan when the plan renews. + @JsonKey(defaultValue: 0) + final int subsequentCommitmentPaymentsCount; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + + return other is InstallmentPlanDetailsWrapper && + other.commitmentPaymentsCount == commitmentPaymentsCount && + other.subsequentCommitmentPaymentsCount == + subsequentCommitmentPaymentsCount; + } + + @override + int get hashCode { + return Object.hash( + commitmentPaymentsCount.hashCode, + subsequentCommitmentPaymentsCount.hashCode, + ); + } +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/subscription_offer_details_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/subscription_offer_details_wrapper.g.dart index 46973ed7d427..e84d871063d4 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/subscription_offer_details_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/subscription_offer_details_wrapper.g.dart @@ -21,6 +21,10 @@ SubscriptionOfferDetailsWrapper _$SubscriptionOfferDetailsWrapperFromJson( Map.from(e as Map))) .toList() ?? [], + installmentPlanDetails: json['installmentPlanDetails'] == null + ? null + : InstallmentPlanDetailsWrapper.fromJson( + Map.from(json['installmentPlanDetails'] as Map)), ); PricingPhaseWrapper _$PricingPhaseWrapperFromJson(Map json) => @@ -35,3 +39,12 @@ PricingPhaseWrapper _$PricingPhaseWrapperFromJson(Map json) => : const RecurrenceModeConverter() .fromJson((json['recurrenceMode'] as num?)?.toInt()), ); + +InstallmentPlanDetailsWrapper _$InstallmentPlanDetailsWrapperFromJson( + Map json) => + InstallmentPlanDetailsWrapper( + commitmentPaymentsCount: + (json['commitmentPaymentsCount'] as num?)?.toInt() ?? 0, + subsequentCommitmentPaymentsCount: + (json['subsequentCommitmentPaymentsCount'] as num?)?.toInt() ?? 0, + ); diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart index 333ccc83bc25..f39d58173674 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart @@ -162,7 +162,6 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { oldProduct: changeSubscriptionParam?.oldPurchaseDetails.productID, purchaseToken: changeSubscriptionParam ?.oldPurchaseDetails.verificationData.serverVerificationData, - prorationMode: changeSubscriptionParam?.prorationMode, replacementMode: changeSubscriptionParam?.replacementMode), ); return billingResultWrapper.responseCode == BillingResponse.ok; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart index aa2f473d2638..cc94285eb98f 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart @@ -31,32 +31,6 @@ class InAppPurchaseAndroidPlatformAddition late final Stream userChoiceDetailsStream = _userChoiceDetailsStreamController.stream; - /// Whether pending purchase is enabled. - /// - /// **Deprecation warning:** it is no longer required to call - /// [enablePendingPurchases] when initializing your application. From now on - /// this is handled internally and the [enablePendingPurchase] property will - /// always return `true`. - /// - /// See also [enablePendingPurchases] for more on pending purchases. - @Deprecated( - 'The requirement to call `enablePendingPurchases()` has become obsolete ' - "since Google Play no longer accepts app submissions that don't support " - 'pending purchases.') - static bool get enablePendingPurchase => true; - - /// Enable the [InAppPurchaseConnection] to handle pending purchases. - /// - /// **Deprecation warning:** it is no longer required to call - /// [enablePendingPurchases] when initializing your application. - @Deprecated( - 'The requirement to call `enablePendingPurchases()` has become obsolete ' - "since Google Play no longer accepts app submissions that don't support " - 'pending purchases.') - static void enablePendingPurchases() { - // No-op, until it is time to completely remove this method from the API. - } - final BillingClientManager _billingClientManager; /// Mark that the user has consumed a product. 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 1e6cca2ccbd3..39d1ba2ff990 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 @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.4.2), do not edit directly. +// Autogenerated from Pigeon (v22.6.1), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -321,7 +321,6 @@ class PlatformBillingConfigResponse { class PlatformBillingFlowParams { PlatformBillingFlowParams({ required this.product, - required this.prorationMode, required this.replacementMode, this.offerToken, this.accountId, @@ -332,8 +331,6 @@ class PlatformBillingFlowParams { String product; - int prorationMode; - int replacementMode; String? offerToken; @@ -349,7 +346,6 @@ class PlatformBillingFlowParams { Object encode() { return [ product, - prorationMode, replacementMode, offerToken, accountId, @@ -363,13 +359,12 @@ class PlatformBillingFlowParams { result as List; return PlatformBillingFlowParams( product: result[0]! as String, - prorationMode: result[1]! as int, - replacementMode: result[2]! as int, - offerToken: result[3] as String?, - accountId: result[4] as String?, - obfuscatedProfileId: result[5] as String?, - oldProduct: result[6] as String?, - purchaseToken: result[7] as String?, + replacementMode: result[1]! as int, + offerToken: result[2] as String?, + accountId: result[3] as String?, + obfuscatedProfileId: result[4] as String?, + oldProduct: result[5] as String?, + purchaseToken: result[6] as String?, ); } } @@ -439,6 +434,7 @@ class PlatformPurchase { required this.quantity, required this.purchaseState, this.accountIdentifiers, + this.pendingPurchaseUpdate, }); String? orderId; @@ -467,6 +463,8 @@ class PlatformPurchase { PlatformAccountIdentifiers? accountIdentifiers; + PlatformPendingPurchaseUpdate? pendingPurchaseUpdate; + Object encode() { return [ orderId, @@ -482,6 +480,7 @@ class PlatformPurchase { quantity, purchaseState, accountIdentifiers, + pendingPurchaseUpdate, ]; } @@ -501,6 +500,36 @@ class PlatformPurchase { quantity: result[10]! as int, purchaseState: result[11]! as PlatformPurchaseState, accountIdentifiers: result[12] as PlatformAccountIdentifiers?, + pendingPurchaseUpdate: result[13] as PlatformPendingPurchaseUpdate?, + ); + } +} + +/// Pigeon version of Java Purchase. +/// +/// See also PendingPurchaseUpdateWrapper on the Dart side. +class PlatformPendingPurchaseUpdate { + PlatformPendingPurchaseUpdate({ + required this.products, + required this.purchaseToken, + }); + + List products; + + String purchaseToken; + + Object encode() { + return [ + products, + purchaseToken, + ]; + } + + static PlatformPendingPurchaseUpdate decode(Object result) { + result as List; + return PlatformPendingPurchaseUpdate( + products: (result[0] as List?)!.cast(), + purchaseToken: result[1]! as String, ); } } @@ -624,6 +653,7 @@ class PlatformSubscriptionOfferDetails { required this.offerToken, required this.offerTags, required this.pricingPhases, + this.installmentPlanDetails, }); String basePlanId; @@ -636,6 +666,8 @@ class PlatformSubscriptionOfferDetails { List pricingPhases; + PlatformInstallmentPlanDetails? installmentPlanDetails; + Object encode() { return [ basePlanId, @@ -643,6 +675,7 @@ class PlatformSubscriptionOfferDetails { offerToken, offerTags, pricingPhases, + installmentPlanDetails, ]; } @@ -655,6 +688,7 @@ class PlatformSubscriptionOfferDetails { offerTags: (result[3] as List?)!.cast(), pricingPhases: (result[4] as List?)!.cast(), + installmentPlanDetails: result[5] as PlatformInstallmentPlanDetails?, ); } } @@ -724,6 +758,56 @@ class PlatformUserChoiceProduct { } } +/// Pigeon version of ProductDetails.InstallmentPlanDetails. +/// https://developer.android.com/reference/com/android/billingclient/api/PendingPurchasesParams.Builder#enableOneTimeProducts() +class PlatformInstallmentPlanDetails { + PlatformInstallmentPlanDetails({ + required this.commitmentPaymentsCount, + required this.subsequentCommitmentPaymentsCount, + }); + + int commitmentPaymentsCount; + + int subsequentCommitmentPaymentsCount; + + Object encode() { + return [ + commitmentPaymentsCount, + subsequentCommitmentPaymentsCount, + ]; + } + + static PlatformInstallmentPlanDetails decode(Object result) { + result as List; + return PlatformInstallmentPlanDetails( + commitmentPaymentsCount: result[0]! as int, + subsequentCommitmentPaymentsCount: result[1]! as int, + ); + } +} + +/// Pigeon version of Java PendingPurchasesParams. +class PendingPurchasesParams { + PendingPurchasesParams({ + required this.enablePrepaidPlans, + }); + + bool enablePrepaidPlans; + + Object encode() { + return [ + enablePrepaidPlans, + ]; + } + + static PendingPurchasesParams decode(Object result) { + result as List; + return PendingPurchasesParams( + enablePrepaidPlans: result[0]! as bool, + ); + } +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -777,24 +861,33 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is PlatformPurchase) { buffer.putUint8(143); writeValue(buffer, value.encode()); - } else if (value is PlatformPurchaseHistoryRecord) { + } else if (value is PlatformPendingPurchaseUpdate) { buffer.putUint8(144); writeValue(buffer, value.encode()); - } else if (value is PlatformPurchaseHistoryResponse) { + } else if (value is PlatformPurchaseHistoryRecord) { buffer.putUint8(145); writeValue(buffer, value.encode()); - } else if (value is PlatformPurchasesResponse) { + } else if (value is PlatformPurchaseHistoryResponse) { buffer.putUint8(146); writeValue(buffer, value.encode()); - } else if (value is PlatformSubscriptionOfferDetails) { + } else if (value is PlatformPurchasesResponse) { buffer.putUint8(147); writeValue(buffer, value.encode()); - } else if (value is PlatformUserChoiceDetails) { + } else if (value is PlatformSubscriptionOfferDetails) { buffer.putUint8(148); writeValue(buffer, value.encode()); - } else if (value is PlatformUserChoiceProduct) { + } else if (value is PlatformUserChoiceDetails) { buffer.putUint8(149); writeValue(buffer, value.encode()); + } else if (value is PlatformUserChoiceProduct) { + buffer.putUint8(150); + writeValue(buffer, value.encode()); + } else if (value is PlatformInstallmentPlanDetails) { + buffer.putUint8(151); + writeValue(buffer, value.encode()); + } else if (value is PendingPurchasesParams) { + buffer.putUint8(152); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -839,17 +932,23 @@ class _PigeonCodec extends StandardMessageCodec { case 143: return PlatformPurchase.decode(readValue(buffer)!); case 144: - return PlatformPurchaseHistoryRecord.decode(readValue(buffer)!); + return PlatformPendingPurchaseUpdate.decode(readValue(buffer)!); case 145: - return PlatformPurchaseHistoryResponse.decode(readValue(buffer)!); + return PlatformPurchaseHistoryRecord.decode(readValue(buffer)!); case 146: - return PlatformPurchasesResponse.decode(readValue(buffer)!); + return PlatformPurchaseHistoryResponse.decode(readValue(buffer)!); case 147: - return PlatformSubscriptionOfferDetails.decode(readValue(buffer)!); + return PlatformPurchasesResponse.decode(readValue(buffer)!); case 148: - return PlatformUserChoiceDetails.decode(readValue(buffer)!); + return PlatformSubscriptionOfferDetails.decode(readValue(buffer)!); case 149: + return PlatformUserChoiceDetails.decode(readValue(buffer)!); + case 150: return PlatformUserChoiceProduct.decode(readValue(buffer)!); + case 151: + return PlatformInstallmentPlanDetails.decode(readValue(buffer)!); + case 152: + return PendingPurchasesParams.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -903,7 +1002,9 @@ class InAppPurchaseApi { /// Wraps BillingClient#startConnection(BillingClientStateListener). Future startConnection( - int callbackHandle, PlatformBillingChoiceMode billingMode) async { + int callbackHandle, + PlatformBillingChoiceMode billingMode, + PendingPurchasesParams pendingPurchasesParams) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.in_app_purchase_android.InAppPurchaseApi.startConnection$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -912,8 +1013,9 @@ class InAppPurchaseApi { pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final List? pigeonVar_replyList = await pigeonVar_channel - .send([callbackHandle, billingMode]) as List?; + final List? pigeonVar_replyList = await pigeonVar_channel.send( + [callbackHandle, billingMode, pendingPurchasesParams]) + as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { 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 bc93f614704f..efe8c7b1080e 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 @@ -4,6 +4,7 @@ import '../billing_client_wrappers.dart'; import 'billing_client_wrappers/billing_config_wrapper.dart'; +import 'billing_client_wrappers/pending_purchases_params_wrapper.dart'; import 'messages.g.dart'; /// Converts a [BillingChoiceMode] to the Pigeon equivalent. @@ -182,9 +183,23 @@ PurchaseWrapper purchaseWrapperFromPlatform(PlatformPurchase purchase) { developerPayload: purchase.developerPayload, obfuscatedAccountId: purchase.accountIdentifiers?.obfuscatedAccountId, obfuscatedProfileId: purchase.accountIdentifiers?.obfuscatedProfileId, + pendingPurchaseUpdate: + pendingPurchaseUpdateFromPlatform(purchase.pendingPurchaseUpdate), ); } +/// Creates a [PendingPurchaseUpdateWrapper] from the Pigeon equivalent. +PendingPurchaseUpdateWrapper? pendingPurchaseUpdateFromPlatform( + PlatformPendingPurchaseUpdate? pendingPurchaseUpdate) { + if (pendingPurchaseUpdate == null) { + return null; + } + + return PendingPurchaseUpdateWrapper( + purchaseToken: pendingPurchaseUpdate.purchaseToken, + products: pendingPurchaseUpdate.products); +} + /// Creates a [PurchaseStateWrapper] from the Pigeon equivalent. PurchaseStateWrapper purchaseStateWrapperFromPlatform( PlatformPurchaseState state) { @@ -215,6 +230,8 @@ SubscriptionOfferDetailsWrapper subscriptionOfferDetailsWrapperFromPlatform( offerIdToken: offer.offerToken, pricingPhases: offer.pricingPhases.map(pricingPhaseWrapperFromPlatform).toList(), + installmentPlanDetails: + installmentPlanDetailsFromPlatform(offer.installmentPlanDetails), ); } @@ -238,3 +255,25 @@ UserChoiceDetailsProductWrapper userChoiceDetailsProductFromPlatform( productType: productTypeFromPlatform(product.type), ); } + +/// Creates a [InstallmentPlanDetailsWrapper] from the Pigeon equivalent. +InstallmentPlanDetailsWrapper? installmentPlanDetailsFromPlatform( + PlatformInstallmentPlanDetails? details) { + if (details == null) { + return null; + } + + return InstallmentPlanDetailsWrapper( + commitmentPaymentsCount: details.commitmentPaymentsCount, + subsequentCommitmentPaymentsCount: + details.subsequentCommitmentPaymentsCount, + ); +} + +/// Converts a [PendingPurchasesParamsWrapper] to its Pigeon equivalent. +PendingPurchasesParams pendingPurchasesParamsFromWrapper( + PendingPurchasesParamsWrapper params) { + return PendingPurchasesParams( + enablePrepaidPlans: params.enablePrepaidPlans, + ); +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/change_subscription_param.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/change_subscription_param.dart index f76fc3e9005b..25907402469b 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/change_subscription_param.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/change_subscription_param.dart @@ -10,7 +10,6 @@ class ChangeSubscriptionParam { /// Creates a new change subscription param object with given data ChangeSubscriptionParam({ required this.oldPurchaseDetails, - @Deprecated('Use replacementMode instead') this.prorationMode, this.replacementMode, }); @@ -18,13 +17,6 @@ class ChangeSubscriptionParam { /// upgrade/downgrade from. final GooglePlayPurchaseDetails oldPurchaseDetails; - /// The proration mode. - /// - /// This is an optional parameter that indicates how to handle the existing - /// subscription when the new subscription comes into effect. - @Deprecated('Use replacementMode instead') - final ProrationMode? prorationMode; - /// The replacement mode. /// /// This is an optional parameter that indicates how to handle the existing 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 3c15cc4750a9..ec0e82936e65 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 @@ -110,7 +110,6 @@ class PlatformBillingConfigResponse { class PlatformBillingFlowParams { PlatformBillingFlowParams({ required this.product, - required this.prorationMode, required this.replacementMode, required this.offerToken, required this.accountId, @@ -123,7 +122,6 @@ class PlatformBillingFlowParams { // Ideally this would be replaced with an enum on the dart side that maps // to constants on the Java side, but it's deprecated anyway so that will be // resolved during the update to the new API. - final int prorationMode; final int replacementMode; final String? offerToken; final String? accountId; @@ -169,6 +167,7 @@ class PlatformPurchase { required this.quantity, required this.purchaseState, required this.accountIdentifiers, + required this.pendingPurchaseUpdate, }); final String? orderId; @@ -184,6 +183,20 @@ class PlatformPurchase { final int quantity; final PlatformPurchaseState purchaseState; final PlatformAccountIdentifiers? accountIdentifiers; + final PlatformPendingPurchaseUpdate? pendingPurchaseUpdate; +} + +/// Pigeon version of Java Purchase. +/// +/// See also PendingPurchaseUpdateWrapper on the Dart side. +class PlatformPendingPurchaseUpdate { + PlatformPendingPurchaseUpdate({ + required this.products, + required this.purchaseToken, + }); + + final List products; + final String purchaseToken; } /// Pigeon version of PurchaseHistoryRecord. @@ -241,6 +254,7 @@ class PlatformSubscriptionOfferDetails { required this.offerToken, required this.offerTags, required this.pricingPhases, + required this.installmentPlanDetails, }); final String basePlanId; @@ -252,6 +266,7 @@ class PlatformSubscriptionOfferDetails { // internal API, we can always add that indirection later if we need it, // so for now this bypasses that unnecessary wrapper. final List pricingPhases; + final PlatformInstallmentPlanDetails? installmentPlanDetails; } /// Pigeon version of UserChoiceDetailsWrapper and Java UserChoiceDetails. @@ -280,6 +295,27 @@ class PlatformUserChoiceProduct { final PlatformProductType type; } +/// Pigeon version of ProductDetails.InstallmentPlanDetails. +/// https://developer.android.com/reference/com/android/billingclient/api/PendingPurchasesParams.Builder#enableOneTimeProducts() +class PlatformInstallmentPlanDetails { + PlatformInstallmentPlanDetails({ + required this.commitmentPaymentsCount, + required this.subsequentCommitmentPaymentsCount, + }); + + final int commitmentPaymentsCount; + final int subsequentCommitmentPaymentsCount; +} + +/// Pigeon version of PendingPurchaseParamsWrapper. +class PendingPurchasesParams { + PendingPurchasesParams({ + required this.enablePrepaidPlans, + }); + + final bool enablePrepaidPlans; +} + /// Pigeon version of Java BillingClient.ProductType. enum PlatformProductType { inapp, @@ -322,7 +358,9 @@ abstract class InAppPurchaseApi { /// Wraps BillingClient#startConnection(BillingClientStateListener). @async PlatformBillingResult startConnection( - int callbackHandle, PlatformBillingChoiceMode billingMode); + int callbackHandle, + PlatformBillingChoiceMode billingMode, + PendingPurchasesParams pendingPurchasesParams); /// Wraps BillingClient#endConnection(BillingClientStateListener). void endConnection(); 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 7e18fa0722a6..a3aae41cb017 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.6+12 +version: 0.4.0 environment: sdk: ^3.5.0 diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart index 1c72d02f99e5..aec6bf1e4580 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase_android/billing_client_wrappers.dart'; +import 'package:in_app_purchase_android/src/billing_client_wrappers/pending_purchases_params_wrapper.dart'; import 'package:in_app_purchase_android/src/messages.g.dart'; import 'package:mockito/mockito.dart'; @@ -21,7 +22,7 @@ void main() { setUp(() { WidgetsFlutterBinding.ensureInitialized(); mockApi = MockInAppPurchaseApi(); - when(mockApi.startConnection(any, any)).thenAnswer( + when(mockApi.startConnection(any, any, any)).thenAnswer( (_) async => PlatformBillingResult(responseCode: 0, debugMessage: '')); manager = BillingClientManager( billingClientFactory: (PurchasesUpdatedListener listener, @@ -32,12 +33,12 @@ void main() { group('BillingClientWrapper', () { test('connects on initialization', () { - verify(mockApi.startConnection(any, any)).called(1); + verify(mockApi.startConnection(any, any, any)).called(1); }); test('waits for connection before executing the operations', () async { final Completer connectedCompleter = Completer(); - when(mockApi.startConnection(any, any)).thenAnswer((_) async { + when(mockApi.startConnection(any, any, any)).thenAnswer((_) async { connectedCompleter.complete(); return PlatformBillingResult(responseCode: 0, debugMessage: ''); }); @@ -64,7 +65,7 @@ void main() { await manager.runWithClientNonRetryable((_) async {}); manager.client.hostCallbackHandler.onBillingServiceDisconnected(0); - verify(mockApi.startConnection(any, any)).called(2); + verify(mockApi.startConnection(any, any, any)).called(2); }); test('re-connects when host calls reconnectWithBillingChoiceMode', @@ -83,11 +84,36 @@ void main() { manager.client.hostCallbackHandler.onBillingServiceDisconnected(0); // Verify that after connection ended reconnect was called. final VerificationResult result = - verify(mockApi.startConnection(any, captureAny)); + verify(mockApi.startConnection(any, captureAny, any)); expect(result.captured.single, PlatformBillingChoiceMode.alternativeBillingOnly); }); + test('re-connects when host calls reconnectWithPendingPurchasesParams', + () async { + // Ensures all asynchronous connected code finishes. + await manager.runWithClientNonRetryable((_) async {}); + + await manager.reconnectWithPendingPurchasesParams( + const PendingPurchasesParamsWrapper(enablePrepaidPlans: true)); + // Verify that connection was ended. + verify(mockApi.endConnection()).called(1); + + clearInteractions(mockApi); + + /// Fake the disconnect that we would expect from a endConnectionCall. + manager.client.hostCallbackHandler.onBillingServiceDisconnected(0); + // Verify that after connection ended reconnect was called. + final VerificationResult result = + verify(mockApi.startConnection(any, any, captureAny)); + expect( + result.captured.single, + isA().having( + (PendingPurchasesParams params) => params.enablePrepaidPlans, + 'enablePrepaidPlans', + true)); + }); + test( 're-connects when operation returns BillingResponse.serviceDisconnected', () async { @@ -104,7 +130,7 @@ void main() { ); }, ); - verify(mockApi.startConnection(any, any)).called(1); + verify(mockApi.startConnection(any, any, any)).called(1); expect(timesCalled, equals(2)); expect(result.responseCode, equals(BillingResponse.ok)); }, @@ -113,7 +139,7 @@ void main() { test('does not re-connect when disposed', () { clearInteractions(mockApi); manager.dispose(); - verifyNever(mockApi.startConnection(any, any)); + verifyNever(mockApi.startConnection(any, any, any)); verify(mockApi.endConnection()).called(1); }); 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 0d94ddc7bf9e..27f274564446 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 @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase_android/billing_client_wrappers.dart'; import 'package:in_app_purchase_android/src/billing_client_wrappers/billing_config_wrapper.dart'; +import 'package:in_app_purchase_android/src/billing_client_wrappers/pending_purchases_params_wrapper.dart'; import 'package:in_app_purchase_android/src/messages.g.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -39,7 +40,7 @@ void main() { setUp(() { mockApi = MockInAppPurchaseApi(); - when(mockApi.startConnection(any, any)).thenAnswer( + when(mockApi.startConnection(any, any, any)).thenAnswer( (_) async => PlatformBillingResult(responseCode: 0, debugMessage: '')); billingClient = BillingClient( (PurchasesResultWrapper _) {}, (UserChoiceDetailsWrapper _) {}, @@ -81,7 +82,7 @@ void main() { test('returns BillingResultWrapper', () async { const String debugMessage = 'dummy message'; const BillingResponse responseCode = BillingResponse.developerError; - when(mockApi.startConnection(any, any)).thenAnswer( + when(mockApi.startConnection(any, any, any)).thenAnswer( (_) async => PlatformBillingResult( responseCode: const BillingResponseConverter().toJson(responseCode), debugMessage: debugMessage, @@ -100,9 +101,15 @@ void main() { await billingClient.startConnection(onBillingServiceDisconnected: () {}); final VerificationResult result = - verify(mockApi.startConnection(captureAny, captureAny)); + verify(mockApi.startConnection(captureAny, captureAny, captureAny)); expect(result.captured[0], 0); expect(result.captured[1], PlatformBillingChoiceMode.playBillingOnly); + expect( + result.captured[2], + isA().having( + (PendingPurchasesParams params) => params.enablePrepaidPlans, + 'enablePrepaidPlans', + false)); }); test('passes billingChoiceMode alternativeBillingOnly when set', () async { @@ -110,7 +117,8 @@ void main() { onBillingServiceDisconnected: () {}, billingChoiceMode: BillingChoiceMode.alternativeBillingOnly); - expect(verify(mockApi.startConnection(any, captureAny)).captured.first, + expect( + verify(mockApi.startConnection(any, captureAny, any)).captured.first, PlatformBillingChoiceMode.alternativeBillingOnly); }); @@ -125,7 +133,8 @@ void main() { onBillingServiceDisconnected: () {}, billingChoiceMode: BillingChoiceMode.alternativeBillingOnly); - expect(verify(mockApi.startConnection(any, captureAny)).captured.first, + expect( + verify(mockApi.startConnection(any, captureAny, any)).captured.first, PlatformBillingChoiceMode.alternativeBillingOnly); const UserChoiceDetailsWrapper expected = UserChoiceDetailsWrapper( @@ -147,6 +156,21 @@ void main() { expect(await completer.future, expected); }); + test('passes pendingPurchasesParams when set', () async { + await billingClient.startConnection( + onBillingServiceDisconnected: () {}, + billingChoiceMode: BillingChoiceMode.alternativeBillingOnly, + pendingPurchasesParams: + const PendingPurchasesParamsWrapper(enablePrepaidPlans: true)); + + expect( + verify(mockApi.startConnection(any, any, captureAny)).captured.first, + isA().having( + (PendingPurchasesParams params) => params.enablePrepaidPlans, + 'enablePrepaidPlans', + true)); + }); + test('UserChoiceDetailsWrapper searilization check', () async { // Test ensures that changes to UserChoiceDetailsWrapper#toJson are // compatible with code in Translator.java. @@ -343,8 +367,8 @@ void main() { const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; const String profileId = 'hashedProfileId'; - const ProrationMode prorationMode = - ProrationMode.immediateAndChargeProratedPrice; + const ReplacementMode replacementMode = + ReplacementMode.chargeProratedPrice; expect( await billingClient.launchBillingFlow( @@ -352,7 +376,7 @@ void main() { accountId: accountId, obfuscatedProfileId: profileId, oldProduct: dummyOldPurchase.products.first, - prorationMode: prorationMode, + replacementMode: replacementMode, purchaseToken: dummyOldPurchase.purchaseToken), equals(expectedBillingResult)); final VerificationResult result = @@ -364,8 +388,8 @@ void main() { expect(params.oldProduct, equals(dummyOldPurchase.products.first)); expect(params.obfuscatedProfileId, equals(profileId)); expect(params.purchaseToken, equals(dummyOldPurchase.purchaseToken)); - expect(params.prorationMode, - const ProrationModeConverter().toJson(prorationMode)); + expect(params.replacementMode, + const ReplacementModeConverter().toJson(replacementMode)); }); test( @@ -380,8 +404,7 @@ void main() { const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; const String profileId = 'hashedProfileId'; - const ProrationMode prorationMode = - ProrationMode.immediateAndChargeFullPrice; + const ReplacementMode replacementMode = ReplacementMode.chargeFullPrice; expect( await billingClient.launchBillingFlow( @@ -389,7 +412,7 @@ void main() { accountId: accountId, obfuscatedProfileId: profileId, oldProduct: dummyOldPurchase.products.first, - prorationMode: prorationMode, + replacementMode: replacementMode, purchaseToken: dummyOldPurchase.purchaseToken), equals(expectedBillingResult)); final VerificationResult result = @@ -401,8 +424,8 @@ void main() { expect(params.oldProduct, equals(dummyOldPurchase.products.first)); expect(params.obfuscatedProfileId, equals(profileId)); expect(params.purchaseToken, equals(dummyOldPurchase.purchaseToken)); - expect(params.prorationMode, - const ProrationModeConverter().toJson(prorationMode)); + expect(params.replacementMode, + const ReplacementModeConverter().toJson(replacementMode)); }); test('handles null accountId', () async { diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart index 5a93a96f9754..810f389c80f5 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.mocks.dart @@ -120,6 +120,7 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { _i4.Future<_i2.PlatformBillingResult> startConnection( int? callbackHandle, _i2.PlatformBillingChoiceMode? billingMode, + _i2.PendingPurchasesParams? pendingPurchasesParams, ) => (super.noSuchMethod( Invocation.method( @@ -127,6 +128,7 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { [ callbackHandle, billingMode, + pendingPurchasesParams, ], ), returnValue: _i4.Future<_i2.PlatformBillingResult>.value( @@ -137,6 +139,7 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { [ callbackHandle, billingMode, + pendingPurchasesParams, ], ), )), @@ -148,6 +151,7 @@ class MockInAppPurchaseApi extends _i1.Mock implements _i2.InAppPurchaseApi { [ callbackHandle, billingMode, + pendingPurchasesParams, ], ), )), 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 3bd6a497490f..7eda49df2f8e 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 @@ -42,6 +42,10 @@ const ProductDetailsWrapper dummySubscriptionProductDetails = recurrenceMode: RecurrenceMode.finiteRecurring, ), ], + installmentPlanDetails: InstallmentPlanDetailsWrapper( + commitmentPaymentsCount: 1, + subsequentCommitmentPaymentsCount: 2, + ), ), ], ); @@ -191,6 +195,10 @@ void main() { recurrenceMode: RecurrenceMode.finiteRecurring, ), ], + installmentPlanDetails: InstallmentPlanDetailsWrapper( + commitmentPaymentsCount: 1, + subsequentCommitmentPaymentsCount: 2, + ), ), ], ); @@ -221,6 +229,10 @@ void main() { recurrenceMode: RecurrenceMode.finiteRecurring, ), ], + installmentPlanDetails: InstallmentPlanDetailsWrapper( + commitmentPaymentsCount: 1, + subsequentCommitmentPaymentsCount: 2, + ), ), ], ); @@ -291,6 +303,8 @@ Map buildSubscriptionMap( 'offerTags': original.offerTags, 'offerIdToken': original.offerIdToken, 'pricingPhases': buildPricingPhaseMapList(original.pricingPhases), + 'installmentPlanDetails': + buildInstallmentPlanMap(original.installmentPlanDetails), }; } @@ -313,3 +327,16 @@ Map buildPricingPhaseMap(PricingPhaseWrapper original) { const RecurrenceModeConverter().toJson(original.recurrenceMode), }; } + +Map? buildInstallmentPlanMap( + InstallmentPlanDetailsWrapper? installmentPlanDetails) { + if (installmentPlanDetails == null) { + return null; + } + + return { + 'commitmentPaymentsCount': installmentPlanDetails.commitmentPaymentsCount, + 'subsequentCommitmentPaymentsCount': + installmentPlanDetails.subsequentCommitmentPaymentsCount, + }; +} 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 14cd446bf8a0..64dfdbb202bc 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 @@ -50,6 +50,25 @@ const PurchaseWrapper dummyUnacknowledgedPurchase = PurchaseWrapper( purchaseState: PurchaseStateWrapper.purchased, ); +const PurchaseWrapper dummyPendingUpdatePurchase = PurchaseWrapper( + orderId: 'orderId', + packageName: 'packageName', + purchaseTime: 0, + signature: 'signature', + products: ['product'], + purchaseToken: 'purchaseToken', + isAutoRenewing: false, + originalJson: '', + developerPayload: 'dummy payload', + isAcknowledged: true, + purchaseState: PurchaseStateWrapper.purchased, + obfuscatedAccountId: 'Account101', + obfuscatedProfileId: 'Profile103', + pendingPurchaseUpdate: PendingPurchaseUpdateWrapper( + purchaseToken: 'pendingPurchaseToken', + products: ['pendingProduct']), +); + const PurchaseHistoryRecordWrapper dummyPurchaseHistoryRecord = PurchaseHistoryRecordWrapper( purchaseTime: 0, @@ -60,6 +79,12 @@ const PurchaseHistoryRecordWrapper dummyPurchaseHistoryRecord = developerPayload: 'dummy payload', ); +const PendingPurchaseUpdateWrapper dummyPendingPurchaseUpdate = + PendingPurchaseUpdateWrapper( + products: ['product'], + purchaseToken: 'purchaseToken', +); + void main() { group('PurchaseWrapper', () { test('converts from map', () { @@ -70,6 +95,14 @@ void main() { expect(parsed, equals(expected)); }); + test('converts from map with pending purchase', () { + const PurchaseWrapper expected = dummyPendingUpdatePurchase; + final PurchaseWrapper parsed = + PurchaseWrapper.fromJson(buildPurchaseMap(expected)); + + expect(parsed, equals(expected)); + }); + test('fromPurchase() should return correct PurchaseDetail object', () { final List details = GooglePlayPurchaseDetails.fromPurchase(dummyMultipleProductsPurchase); @@ -212,6 +245,17 @@ void main() { expect(parsed.purchaseHistoryRecordList, isEmpty); }); }); + + group('PendingPurchaseUpdateWrapper', () { + test('converts from map', () { + const PendingPurchaseUpdateWrapper expected = dummyPendingPurchaseUpdate; + final PendingPurchaseUpdateWrapper parsed = + PendingPurchaseUpdateWrapper.fromJson( + buildPendingPurchaseUpdateMap(expected)!); + + expect(parsed, equals(expected)); + }); + }); } Map buildPurchaseMap(PurchaseWrapper original) { @@ -230,6 +274,8 @@ Map buildPurchaseMap(PurchaseWrapper original) { 'isAcknowledged': original.isAcknowledged, 'obfuscatedAccountId': original.obfuscatedAccountId, 'obfuscatedProfileId': original.obfuscatedProfileId, + 'pendingPurchaseUpdate': + buildPendingPurchaseUpdateMap(original.pendingPurchaseUpdate), }; } @@ -252,3 +298,15 @@ Map buildBillingResultMap(BillingResultWrapper original) { 'debugMessage': original.debugMessage, }; } + +Map? buildPendingPurchaseUpdateMap( + PendingPurchaseUpdateWrapper? original) { + if (original == null) { + return null; + } + + return { + 'products': original.products, + 'purchaseToken': original.purchaseToken, + }; +} diff --git a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart index a9c76ab5afef..17e7d28d6f04 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart @@ -27,7 +27,7 @@ void main() { setUp(() { widgets.WidgetsFlutterBinding.ensureInitialized(); mockApi = MockInAppPurchaseApi(); - when(mockApi.startConnection(any, any)).thenAnswer( + when(mockApi.startConnection(any, any, any)).thenAnswer( (_) async => PlatformBillingResult(responseCode: 0, debugMessage: '')); manager = BillingClientManager( billingClientFactory: (PurchasesUpdatedListener listener, @@ -80,7 +80,7 @@ void main() { manager.client.hostCallbackHandler.onBillingServiceDisconnected(0); // Verify that after connection ended reconnect was called. final VerificationResult result = - verify(mockApi.startConnection(any, captureAny)); + verify(mockApi.startConnection(any, captureAny, any)); expect(result.callCount, equals(2)); expect(result.captured.last, PlatformBillingChoiceMode.alternativeBillingOnly); @@ -95,7 +95,7 @@ void main() { manager.client.hostCallbackHandler.onBillingServiceDisconnected(0); // Verify that after connection ended reconnect was called. final VerificationResult result = - verify(mockApi.startConnection(any, captureAny)); + verify(mockApi.startConnection(any, captureAny, any)); expect(result.callCount, equals(2)); expect(result.captured.last, PlatformBillingChoiceMode.playBillingOnly); }); 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 ee183eeba71a..6d0c19344140 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 @@ -30,7 +30,7 @@ void main() { widgets.WidgetsFlutterBinding.ensureInitialized(); mockApi = MockInAppPurchaseApi(); - when(mockApi.startConnection(any, any)).thenAnswer( + when(mockApi.startConnection(any, any, any)).thenAnswer( (_) async => PlatformBillingResult(responseCode: 0, debugMessage: '')); iapAndroidPlatform = InAppPurchaseAndroidPlatform( manager: BillingClientManager( @@ -45,13 +45,13 @@ void main() { group('connection management', () { test('connects on initialization', () { //await iapAndroidPlatform.isAvailable(); - verify(mockApi.startConnection(any, any)).called(1); + verify(mockApi.startConnection(any, any, any)).called(1); }); test('re-connects when client sends onBillingServiceDisconnected', () { iapAndroidPlatform.billingClientManager.client.hostCallbackHandler .onBillingServiceDisconnected(0); - verify(mockApi.startConnection(any, any)).called(2); + verify(mockApi.startConnection(any, any, any)).called(2); }); test( @@ -63,7 +63,7 @@ void main() { .toJson(BillingResponse.serviceDisconnected), debugMessage: 'disconnected'), ); - when(mockApi.startConnection(any, any)).thenAnswer((_) async { + when(mockApi.startConnection(any, any, any)).thenAnswer((_) async { // Change the acknowledgePurchase response to success for the next call. when(mockApi.acknowledgePurchase(any)).thenAnswer( (_) async => PlatformBillingResult( @@ -79,7 +79,7 @@ void main() { final BillingResultWrapper result = await iapAndroidPlatform.completePurchase(purchase); verify(mockApi.acknowledgePurchase(any)).called(2); - verify(mockApi.startConnection(any, any)).called(2); + verify(mockApi.startConnection(any, any, any)).called(2); expect(result.responseCode, equals(BillingResponse.ok)); }); }); @@ -717,7 +717,7 @@ void main() { oldPurchaseDetails: GooglePlayPurchaseDetails.fromPurchase( dummyUnacknowledgedPurchase) .first, - prorationMode: ProrationMode.deferred, + replacementMode: ReplacementMode.deferred, )); await iapAndroidPlatform.buyNonConsumable(purchaseParam: purchaseParam);