From 9c2020a00bd416b0e48d043c36f01c27c3d70cb0 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 24 Jun 2021 10:31:33 +0200 Subject: [PATCH 1/4] Add documentation for launchPriceChangeConfirmationFlow --- .../in_app_purchase/in_app_purchase/README.md | 49 +++++++++++++++++++ .../in_app_purchase/example/lib/main.dart | 20 ++++++++ .../in_app_purchase/pubspec.yaml | 2 +- 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/packages/in_app_purchase/in_app_purchase/README.md b/packages/in_app_purchase/in_app_purchase/README.md index 28b3c0821cf3..15ec76f671cb 100644 --- a/packages/in_app_purchase/in_app_purchase/README.md +++ b/packages/in_app_purchase/in_app_purchase/README.md @@ -247,6 +247,55 @@ InAppPurchase.instance .buyNonConsumable(purchaseParam: purchaseParam); ``` +### Confirming subscription price changes + +When you change the price of a subscription the user will need to confirm the price change. If the user does not +confirm the price change the subscription will not be auto-renewed. On both iOS and Android the user will +automatically get a popup to confirm the price change, but you as a developer can show the popup on a moment that +suits you better. This works different on the Apple App Store and on the Google Play Store. + +#### Google Play Store (Android) +When you raise the subscription price you will have 7 days to ask the user to approve the new price. The official +documentation can be found [here](https://support.google.com/googleplay/android-developer/answer/140504?hl=en#zippy=%2Cprice-changes). +When you lower the price the user will automatically receive the lower price and does not have to approve the price +change. + +After 7 days the user will be notified through email and notifications on Google Play to agree with the new price, +so you have 7 days to explain the user in your app that the price is going to change and ask them to accept the +change. You will have to keep track of whether or not the price change is already accepted in your app or in the +backend. The [Google Play API](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptions) +can be used to check whether or not the price change is accepted by the user by reading the priceChange property on +a subscription object. + +The InAppPurchaseAndroidPlatformAddition can be used to show the price change confirmation flow. The additions +contain the function `launchPriceChangeConfirmationFlow` which needs the sku code of the subscription. + +```dart +//import for InAppPurchaseAndroidPlatformAddition +import 'package:in_app_purchase_android/in_app_purchase_android.dart'; +//import for BillingResponse +import 'package:in_app_purchase_android/billing_client_wrappers.dart'; + +if (Platform.isAndroid) { + final InAppPurchaseAndroidPlatformAddition androidAddition = + _inAppPurchase + .getPlatformAddition(); + var priceChangeConfirmationResult = + await androidAddition.launchPriceChangeConfirmationFlow( + sku: 'purchaseId', + ); + if (priceChangeConfirmationResult.responseCode == BillingResponse.ok){ + // TODO acknowledge price change + }else{ + // TODO show error + } +} +``` + +#### Apple App Store (iOS) + +//TODO + ### Accessing platform specific product or purchase properties The function `_inAppPurchase.queryProductDetails(productIds);` provides a `ProductDetailsResponse` with a 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 5429a00125ac..26eda982dba7 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 @@ -438,6 +438,26 @@ class _MyAppState extends State<_MyApp> { }); } + Future confirmPriceChange() async { + if (Platform.isAndroid) { + final InAppPurchaseAndroidPlatformAddition androidAddition = + _inAppPurchase + .getPlatformAddition(); + var priceChangeConfirmationResult = + await androidAddition.launchPriceChangeConfirmationFlow( + sku: 'purchaseId', + ); + if (priceChangeConfirmationResult.responseCode == BillingResponse.ok) { + // TODO acknowledge price change + } else { + // TODO show error + } + } + if(Platform.isIOS){ + //TODO setup flow + } + } + GooglePlayPurchaseDetails? _getOldSubscription( ProductDetails productDetails, Map purchases) { // This is just to demonstrate a subscription upgrade or downgrade. diff --git a/packages/in_app_purchase/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/in_app_purchase/pubspec.yaml index b589c24d3677..a30c9feaeb8d 100644 --- a/packages/in_app_purchase/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: flutter: sdk: flutter in_app_purchase_platform_interface: ^1.0.0 - in_app_purchase_android: ^0.1.0 + in_app_purchase_android: ^0.1.4 in_app_purchase_ios: ^0.1.0 dev_dependencies: From 55e6c183efc94768b06f60a19128b46c42e70175 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Mon, 28 Jun 2021 18:21:52 +0200 Subject: [PATCH 2/4] Textual suggestions from Maurits Co-authored-by: Maurits van Beusekom --- .../in_app_purchase/in_app_purchase/README.md | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase/README.md b/packages/in_app_purchase/in_app_purchase/README.md index 15ec76f671cb..ababb80572ad 100644 --- a/packages/in_app_purchase/in_app_purchase/README.md +++ b/packages/in_app_purchase/in_app_purchase/README.md @@ -249,26 +249,18 @@ InAppPurchase.instance ### Confirming subscription price changes -When you change the price of a subscription the user will need to confirm the price change. If the user does not -confirm the price change the subscription will not be auto-renewed. On both iOS and Android the user will -automatically get a popup to confirm the price change, but you as a developer can show the popup on a moment that -suits you better. This works different on the Apple App Store and on the Google Play Store. +When the price of a subscription is changed the consumer will need to confirm that price change. If the consumer does not +confirm the price change the subscription will not be auto-renewed. By default on both iOS and Android the consumer will +automatically get a popup to confirm the price change, but App developers can override this mechanism and show the popup on a later moment so it doesn't interrupt the critical flow of the App. This works different on the Apple App Store and on the Google Play Store. #### Google Play Store (Android) -When you raise the subscription price you will have 7 days to ask the user to approve the new price. The official +When the subscription price is raised, the consumer should approve the price change within 7 days. The official documentation can be found [here](https://support.google.com/googleplay/android-developer/answer/140504?hl=en#zippy=%2Cprice-changes). -When you lower the price the user will automatically receive the lower price and does not have to approve the price -change. - -After 7 days the user will be notified through email and notifications on Google Play to agree with the new price, -so you have 7 days to explain the user in your app that the price is going to change and ask them to accept the -change. You will have to keep track of whether or not the price change is already accepted in your app or in the -backend. The [Google Play API](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptions) -can be used to check whether or not the price change is accepted by the user by reading the priceChange property on -a subscription object. - -The InAppPurchaseAndroidPlatformAddition can be used to show the price change confirmation flow. The additions -contain the function `launchPriceChangeConfirmationFlow` which needs the sku code of the subscription. +When the price is lowered the consumer will automatically receive the lower price and does not have to approve the price change. + +After 7 days the consumer will be notified through email and notifications on Google Play to agree with the new price. App developers have 7 days to explain the consumer that the price is going to change and ask them to accept this change. App developers have to keep track of whether or not the price change is already accepted within the app or in the backend. The [Google Play API](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptions) can be used to check whether or not the price change is accepted by the consumer by reading the `priceChange` property on a subscription object. + +The `InAppPurchaseAndroidPlatformAddition` can be used to show the price change confirmation flow. The additions contain the function `launchPriceChangeConfirmationFlow` which needs the SKU code of the subscription. ```dart //import for InAppPurchaseAndroidPlatformAddition From d171d4d36ee40792918f1bade7330144896eba34 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 29 Jun 2021 10:53:15 +0200 Subject: [PATCH 3/4] Add iOS example --- .../in_app_purchase/in_app_purchase/README.md | 62 ++++++++++++++++++- .../in_app_purchase/example/lib/main.dart | 37 ++++++++++- .../in_app_purchase/pubspec.yaml | 2 +- 3 files changed, 97 insertions(+), 4 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase/README.md b/packages/in_app_purchase/in_app_purchase/README.md index ababb80572ad..61803e35ebdc 100644 --- a/packages/in_app_purchase/in_app_purchase/README.md +++ b/packages/in_app_purchase/in_app_purchase/README.md @@ -286,7 +286,67 @@ if (Platform.isAndroid) { #### Apple App Store (iOS) -//TODO +When the price of a subscription is raised iOS will also show a popup in the app. +The StoreKit Payment Queue will notify the app that it wants to show a price change confirmation popup. +By default the queue will get the response that it can continue and show the popup. +However, it is possible to prevent this popup via the InAppPurchaseIosPlatformAddition and show the +popup at a different time, for example after clicking a button. + +To know when the App Store wants to show a popup and prevent this from happening a queue delegate can be registered. +The `InAppPurchaseIosPlatformAddition` contains a `setDelegate(SKPaymentQueueDelegateWrapper? delegate)` function that +can be used to set a delegate or remove one by setting it to `null`. +```dart +//import for InAppPurchaseIosPlatformAddition +import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; + +Future initStoreInfo() async { + if (Platform.isIOS) { + var iosPlatformAddition = _inAppPurchase + .getPlatformAddition(); + await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate()); + } +} + +@override +Future disposeStore() { + if (Platform.isIOS) { + var iosPlatformAddition = _inAppPurchase + .getPlatformAddition(); + await iosPlatformAddition.setDelegate(null); + } +} +``` +The delegate that is set should implement `SKPaymentQueueDelegateWrapper` and handle `shouldContinueTransaction` and +`shouldShowPriceConsent`. When setting `shouldShowPriceConsent` to false the default popup will not be shown and the app +needs to show this later. + +```dart +// import for SKPaymentQueueDelegateWrapper +import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; + +class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper { + @override + bool shouldContinueTransaction( + SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) { + return true; + } + + @override + bool shouldShowPriceConsent() { + return false; + } +} +``` + +The dialog can be shown by calling `showPriceConsentIfNeeded` on the `InAppPurchaseIosPlatformAddition`. This future +will complete immediately when the dialog is shown. A confirmed transaction will be delivered on the `purchaseStream`. +```dart +if (Platform.isIOS) { + var iapIosPlatformAddition = _inAppPurchase + .getPlatformAddition(); + await iapIosPlatformAddition.showPriceConsentIfNeeded(); +} +``` ### Accessing platform specific product or purchase properties 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 26eda982dba7..b20575573034 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 @@ -9,6 +9,8 @@ import 'package:flutter/material.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:in_app_purchase_android/billing_client_wrappers.dart'; import 'package:in_app_purchase_android/in_app_purchase_android.dart'; +import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; +import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; import 'consumable_store.dart'; void main() { @@ -84,6 +86,12 @@ class _MyAppState extends State<_MyApp> { return; } + if (Platform.isIOS) { + var iosPlatformAddition = _inAppPurchase + .getPlatformAddition(); + await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate()); + } + ProductDetailsResponse productDetailResponse = await _inAppPurchase.queryProductDetails(_kProductIds.toSet()); if (productDetailResponse.error != null) { @@ -127,6 +135,11 @@ class _MyAppState extends State<_MyApp> { @override void dispose() { + if (Platform.isIOS) { + var iosPlatformAddition = _inAppPurchase + .getPlatformAddition(); + iosPlatformAddition.setDelegate(null); + } _subscription.cancel(); super.dispose(); } @@ -453,8 +466,10 @@ class _MyAppState extends State<_MyApp> { // TODO show error } } - if(Platform.isIOS){ - //TODO setup flow + if (Platform.isIOS) { + var iapIosPlatformAddition = _inAppPurchase + .getPlatformAddition(); + await iapIosPlatformAddition.showPriceConsentIfNeeded(); } } @@ -480,3 +495,21 @@ class _MyAppState extends State<_MyApp> { return oldSubscription; } } + +/// Example implementation of the +/// [`SKPaymentQueueDelegate`](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate?language=objc). +/// +/// The payment queue delegate can be implementated to provide information +/// needed to complete transactions. +class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper { + @override + bool shouldContinueTransaction( + SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) { + return true; + } + + @override + bool shouldShowPriceConsent() { + return false; + } +} diff --git a/packages/in_app_purchase/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/in_app_purchase/pubspec.yaml index a30c9feaeb8d..554a07b0bd30 100644 --- a/packages/in_app_purchase/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: sdk: flutter in_app_purchase_platform_interface: ^1.0.0 in_app_purchase_android: ^0.1.4 - in_app_purchase_ios: ^0.1.0 + in_app_purchase_ios: ^0.1.1 dev_dependencies: flutter_driver: From 4b62107df09d8ed42b7676de791ada30640c48de Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 8 Jul 2021 13:53:48 +0200 Subject: [PATCH 4/4] Added snackbar messages for price change confirmation --- .../in_app_purchase/example/lib/main.dart | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) 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 b20575573034..73ecadb3f15d 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 @@ -258,7 +258,9 @@ class _MyAppState extends State<_MyApp> { productDetails.description, ), trailing: previousPurchase != null - ? Icon(Icons.check) + ? IconButton( + onPressed: () => confirmPriceChange(context), + icon: Icon(Icons.upgrade)) : TextButton( child: Text(productDetails.price), style: TextButton.styleFrom( @@ -451,7 +453,7 @@ class _MyAppState extends State<_MyApp> { }); } - Future confirmPriceChange() async { + Future confirmPriceChange(BuildContext context) async { if (Platform.isAndroid) { final InAppPurchaseAndroidPlatformAddition androidAddition = _inAppPurchase @@ -461,9 +463,16 @@ class _MyAppState extends State<_MyApp> { sku: 'purchaseId', ); if (priceChangeConfirmationResult.responseCode == BillingResponse.ok) { - // TODO acknowledge price change + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Price change accepted'), + )); } else { - // TODO show error + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + priceChangeConfirmationResult.debugMessage ?? + "Price change failed with code ${priceChangeConfirmationResult.responseCode}", + ), + )); } } if (Platform.isIOS) {