Skip to content

Commit

Permalink
[in_app_purchase] Add documentation for price change confirmations (f…
Browse files Browse the repository at this point in the history
  • Loading branch information
renefloor authored and fotiDim committed Sep 13, 2021
1 parent f794df0 commit cd5f38e
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 3 deletions.
101 changes: 101 additions & 0 deletions packages/in_app_purchase/in_app_purchase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,107 @@ InAppPurchase.instance
.buyNonConsumable(purchaseParam: purchaseParam);
```

### Confirming subscription price changes

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 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 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
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<InAppPurchaseAndroidPlatformAddition>();
var priceChangeConfirmationResult =
await androidAddition.launchPriceChangeConfirmationFlow(
sku: 'purchaseId',
);
if (priceChangeConfirmationResult.responseCode == BillingResponse.ok){
// TODO acknowledge price change
}else{
// TODO show error
}
}
```

#### Apple App Store (iOS)

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<void> initStoreInfo() async {
if (Platform.isIOS) {
var iosPlatformAddition = _inAppPurchase
.getPlatformAddition<InAppPurchaseIosPlatformAddition>();
await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate());
}
}
@override
Future<void> disposeStore() {
if (Platform.isIOS) {
var iosPlatformAddition = _inAppPurchase
.getPlatformAddition<InAppPurchaseIosPlatformAddition>();
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<InAppPurchaseIosPlatformAddition>();
await iapIosPlatformAddition.showPriceConsentIfNeeded();
}
```

### Accessing platform specific product or purchase properties

The function `_inAppPurchase.queryProductDetails(productIds);` provides a `ProductDetailsResponse` with a
Expand Down
64 changes: 63 additions & 1 deletion packages/in_app_purchase/in_app_purchase/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -84,6 +86,12 @@ class _MyAppState extends State<_MyApp> {
return;
}

if (Platform.isIOS) {
var iosPlatformAddition = _inAppPurchase
.getPlatformAddition<InAppPurchaseIosPlatformAddition>();
await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate());
}

ProductDetailsResponse productDetailResponse =
await _inAppPurchase.queryProductDetails(_kProductIds.toSet());
if (productDetailResponse.error != null) {
Expand Down Expand Up @@ -127,6 +135,11 @@ class _MyAppState extends State<_MyApp> {

@override
void dispose() {
if (Platform.isIOS) {
var iosPlatformAddition = _inAppPurchase
.getPlatformAddition<InAppPurchaseIosPlatformAddition>();
iosPlatformAddition.setDelegate(null);
}
_subscription.cancel();
super.dispose();
}
Expand Down Expand Up @@ -245,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(
Expand Down Expand Up @@ -438,6 +453,35 @@ class _MyAppState extends State<_MyApp> {
});
}

Future<void> confirmPriceChange(BuildContext context) async {
if (Platform.isAndroid) {
final InAppPurchaseAndroidPlatformAddition androidAddition =
_inAppPurchase
.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
var priceChangeConfirmationResult =
await androidAddition.launchPriceChangeConfirmationFlow(
sku: 'purchaseId',
);
if (priceChangeConfirmationResult.responseCode == BillingResponse.ok) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Price change accepted'),
));
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
priceChangeConfirmationResult.debugMessage ??
"Price change failed with code ${priceChangeConfirmationResult.responseCode}",
),
));
}
}
if (Platform.isIOS) {
var iapIosPlatformAddition = _inAppPurchase
.getPlatformAddition<InAppPurchaseIosPlatformAddition>();
await iapIosPlatformAddition.showPriceConsentIfNeeded();
}
}

GooglePlayPurchaseDetails? _getOldSubscription(
ProductDetails productDetails, Map<String, PurchaseDetails> purchases) {
// This is just to demonstrate a subscription upgrade or downgrade.
Expand All @@ -460,3 +504,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;
}
}
4 changes: 2 additions & 2 deletions packages/in_app_purchase/in_app_purchase/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ dependencies:
flutter:
sdk: flutter
in_app_purchase_platform_interface: ^1.0.0
in_app_purchase_android: ^0.1.0
in_app_purchase_ios: ^0.1.0
in_app_purchase_android: ^0.1.4
in_app_purchase_ios: ^0.1.1

dev_dependencies:
flutter_driver:
Expand Down

0 comments on commit cd5f38e

Please sign in to comment.