Skip to content

Commit cd5f38e

Browse files
renefloorfotiDim
authored andcommitted
[in_app_purchase] Add documentation for price change confirmations (flutter#4092)
1 parent f794df0 commit cd5f38e

3 files changed

Lines changed: 166 additions & 3 deletions

File tree

packages/in_app_purchase/in_app_purchase/README.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,107 @@ InAppPurchase.instance
247247
.buyNonConsumable(purchaseParam: purchaseParam);
248248
```
249249

250+
### Confirming subscription price changes
251+
252+
When the price of a subscription is changed the consumer will need to confirm that price change. If the consumer does not
253+
confirm the price change the subscription will not be auto-renewed. By default on both iOS and Android the consumer will
254+
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.
255+
256+
#### Google Play Store (Android)
257+
When the subscription price is raised, the consumer should approve the price change within 7 days. The official
258+
documentation can be found [here](https://support.google.com/googleplay/android-developer/answer/140504?hl=en#zippy=%2Cprice-changes).
259+
When the price is lowered the consumer will automatically receive the lower price and does not have to approve the price change.
260+
261+
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.
262+
263+
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.
264+
265+
```dart
266+
//import for InAppPurchaseAndroidPlatformAddition
267+
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
268+
//import for BillingResponse
269+
import 'package:in_app_purchase_android/billing_client_wrappers.dart';
270+
271+
if (Platform.isAndroid) {
272+
final InAppPurchaseAndroidPlatformAddition androidAddition =
273+
_inAppPurchase
274+
.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
275+
var priceChangeConfirmationResult =
276+
await androidAddition.launchPriceChangeConfirmationFlow(
277+
sku: 'purchaseId',
278+
);
279+
if (priceChangeConfirmationResult.responseCode == BillingResponse.ok){
280+
// TODO acknowledge price change
281+
}else{
282+
// TODO show error
283+
}
284+
}
285+
```
286+
287+
#### Apple App Store (iOS)
288+
289+
When the price of a subscription is raised iOS will also show a popup in the app.
290+
The StoreKit Payment Queue will notify the app that it wants to show a price change confirmation popup.
291+
By default the queue will get the response that it can continue and show the popup.
292+
However, it is possible to prevent this popup via the InAppPurchaseIosPlatformAddition and show the
293+
popup at a different time, for example after clicking a button.
294+
295+
To know when the App Store wants to show a popup and prevent this from happening a queue delegate can be registered.
296+
The `InAppPurchaseIosPlatformAddition` contains a `setDelegate(SKPaymentQueueDelegateWrapper? delegate)` function that
297+
can be used to set a delegate or remove one by setting it to `null`.
298+
```dart
299+
//import for InAppPurchaseIosPlatformAddition
300+
import 'package:in_app_purchase_ios/in_app_purchase_ios.dart';
301+
302+
Future<void> initStoreInfo() async {
303+
if (Platform.isIOS) {
304+
var iosPlatformAddition = _inAppPurchase
305+
.getPlatformAddition<InAppPurchaseIosPlatformAddition>();
306+
await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate());
307+
}
308+
}
309+
310+
@override
311+
Future<void> disposeStore() {
312+
if (Platform.isIOS) {
313+
var iosPlatformAddition = _inAppPurchase
314+
.getPlatformAddition<InAppPurchaseIosPlatformAddition>();
315+
await iosPlatformAddition.setDelegate(null);
316+
}
317+
}
318+
```
319+
The delegate that is set should implement `SKPaymentQueueDelegateWrapper` and handle `shouldContinueTransaction` and
320+
`shouldShowPriceConsent`. When setting `shouldShowPriceConsent` to false the default popup will not be shown and the app
321+
needs to show this later.
322+
323+
```dart
324+
// import for SKPaymentQueueDelegateWrapper
325+
import 'package:in_app_purchase_ios/store_kit_wrappers.dart';
326+
327+
class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper {
328+
@override
329+
bool shouldContinueTransaction(
330+
SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) {
331+
return true;
332+
}
333+
334+
@override
335+
bool shouldShowPriceConsent() {
336+
return false;
337+
}
338+
}
339+
```
340+
341+
The dialog can be shown by calling `showPriceConsentIfNeeded` on the `InAppPurchaseIosPlatformAddition`. This future
342+
will complete immediately when the dialog is shown. A confirmed transaction will be delivered on the `purchaseStream`.
343+
```dart
344+
if (Platform.isIOS) {
345+
var iapIosPlatformAddition = _inAppPurchase
346+
.getPlatformAddition<InAppPurchaseIosPlatformAddition>();
347+
await iapIosPlatformAddition.showPriceConsentIfNeeded();
348+
}
349+
```
350+
250351
### Accessing platform specific product or purchase properties
251352

252353
The function `_inAppPurchase.queryProductDetails(productIds);` provides a `ProductDetailsResponse` with a

packages/in_app_purchase/in_app_purchase/example/lib/main.dart

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import 'package:flutter/material.dart';
99
import 'package:in_app_purchase/in_app_purchase.dart';
1010
import 'package:in_app_purchase_android/billing_client_wrappers.dart';
1111
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
12+
import 'package:in_app_purchase_ios/in_app_purchase_ios.dart';
13+
import 'package:in_app_purchase_ios/store_kit_wrappers.dart';
1214
import 'consumable_store.dart';
1315

1416
void main() {
@@ -84,6 +86,12 @@ class _MyAppState extends State<_MyApp> {
8486
return;
8587
}
8688

89+
if (Platform.isIOS) {
90+
var iosPlatformAddition = _inAppPurchase
91+
.getPlatformAddition<InAppPurchaseIosPlatformAddition>();
92+
await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate());
93+
}
94+
8795
ProductDetailsResponse productDetailResponse =
8896
await _inAppPurchase.queryProductDetails(_kProductIds.toSet());
8997
if (productDetailResponse.error != null) {
@@ -127,6 +135,11 @@ class _MyAppState extends State<_MyApp> {
127135

128136
@override
129137
void dispose() {
138+
if (Platform.isIOS) {
139+
var iosPlatformAddition = _inAppPurchase
140+
.getPlatformAddition<InAppPurchaseIosPlatformAddition>();
141+
iosPlatformAddition.setDelegate(null);
142+
}
130143
_subscription.cancel();
131144
super.dispose();
132145
}
@@ -245,7 +258,9 @@ class _MyAppState extends State<_MyApp> {
245258
productDetails.description,
246259
),
247260
trailing: previousPurchase != null
248-
? Icon(Icons.check)
261+
? IconButton(
262+
onPressed: () => confirmPriceChange(context),
263+
icon: Icon(Icons.upgrade))
249264
: TextButton(
250265
child: Text(productDetails.price),
251266
style: TextButton.styleFrom(
@@ -438,6 +453,35 @@ class _MyAppState extends State<_MyApp> {
438453
});
439454
}
440455

456+
Future<void> confirmPriceChange(BuildContext context) async {
457+
if (Platform.isAndroid) {
458+
final InAppPurchaseAndroidPlatformAddition androidAddition =
459+
_inAppPurchase
460+
.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
461+
var priceChangeConfirmationResult =
462+
await androidAddition.launchPriceChangeConfirmationFlow(
463+
sku: 'purchaseId',
464+
);
465+
if (priceChangeConfirmationResult.responseCode == BillingResponse.ok) {
466+
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
467+
content: Text('Price change accepted'),
468+
));
469+
} else {
470+
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
471+
content: Text(
472+
priceChangeConfirmationResult.debugMessage ??
473+
"Price change failed with code ${priceChangeConfirmationResult.responseCode}",
474+
),
475+
));
476+
}
477+
}
478+
if (Platform.isIOS) {
479+
var iapIosPlatformAddition = _inAppPurchase
480+
.getPlatformAddition<InAppPurchaseIosPlatformAddition>();
481+
await iapIosPlatformAddition.showPriceConsentIfNeeded();
482+
}
483+
}
484+
441485
GooglePlayPurchaseDetails? _getOldSubscription(
442486
ProductDetails productDetails, Map<String, PurchaseDetails> purchases) {
443487
// This is just to demonstrate a subscription upgrade or downgrade.
@@ -460,3 +504,21 @@ class _MyAppState extends State<_MyApp> {
460504
return oldSubscription;
461505
}
462506
}
507+
508+
/// Example implementation of the
509+
/// [`SKPaymentQueueDelegate`](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate?language=objc).
510+
///
511+
/// The payment queue delegate can be implementated to provide information
512+
/// needed to complete transactions.
513+
class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper {
514+
@override
515+
bool shouldContinueTransaction(
516+
SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) {
517+
return true;
518+
}
519+
520+
@override
521+
bool shouldShowPriceConsent() {
522+
return false;
523+
}
524+
}

packages/in_app_purchase/in_app_purchase/pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ dependencies:
2020
flutter:
2121
sdk: flutter
2222
in_app_purchase_platform_interface: ^1.0.0
23-
in_app_purchase_android: ^0.1.0
24-
in_app_purchase_ios: ^0.1.0
23+
in_app_purchase_android: ^0.1.4
24+
in_app_purchase_ios: ^0.1.1
2525

2626
dev_dependencies:
2727
flutter_driver:

0 commit comments

Comments
 (0)