diff --git a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md index 114f11aa89b3..beb4356dc398 100644 --- a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md @@ -1,10 +1,14 @@ +## 0.5.1+2 + +* Update README to provide a better instruction of the plugin. + ## 0.5.1+1 * Fix error message when trying to consume purchase on iOS. ## 0.5.1 -* [iOS] Introduce `SKPaymentQueueWrapper.presentCodeRedemptionSheet` +* [iOS] Introduce `SKPaymentQueueWrapper.presentCodeRedemptionSheet` ## 0.5.0 diff --git a/packages/in_app_purchase/in_app_purchase/README.md b/packages/in_app_purchase/in_app_purchase/README.md index 62adaa9b4dec..a51187e792ab 100644 --- a/packages/in_app_purchase/in_app_purchase/README.md +++ b/packages/in_app_purchase/in_app_purchase/README.md @@ -1,56 +1,74 @@ -# In App Purchase +A storefront-independent API for purchases in Flutter apps. -A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases -through the App Store (on iOS) and Google Play (on Android). + + +This plugin supports in-app purchases (_IAP_) through an _underlying store_, +which can be the App Store (on iOS) or Google Play (on Android). + +> This plugin is in beta. Use it with caution and +> [file any potential issues you see](https://github.com/flutter/flutter/issues/new/choose). + +

+ An animated image of the iOS in-app purchase UI +      + An animated image of the Android in-app purchase UI +

## Features -Add this to your Flutter app to: +Use this plugin in your Flutter app to: -1. Show in app products that are available for sale from the underlying shop. - Includes consumables, permanent upgrades, and subscriptions. -2. Load in app products currently owned by the user according to the underlying - shop. -3. Send your user to the underlying store to purchase your products. +* Show in-app products that are available for sale from the underlying store. + Products can include consumables, permanent upgrades, and subscriptions. +* Load in-app products that the user owns. +* Send the user to the underlying store to purchase products. +* Present a UI for redeeming subscription offer codes. (iOS 14 only) -## Getting Started +## Getting started -This plugin is in beta. Please use with caution and file any potential issues -you see on our [issue tracker](https://github.com/flutter/flutter/issues/new/choose). +This plugin relies on the App Store and Google Play for making in-app purchases. +It exposes a unified surface, but you still need to understand and configure +your app with each store. Both stores have extensive guides: -This plugin relies on the App Store and Google Play for making in app purchases. -It exposes a unified surface, but you'll still need to understand and configure -your app with each store to handle purchases using them. Both have extensive -guides: +* [App Store documentation](https://developer.apple.com/in-app-purchase/) +* [Google Play documentation](https://developer.android.com/google/play/billing/billing_overview) -* [In-App Purchase (App Store)](https://developer.apple.com/in-app-purchase/) -* [Google Play Billing Overview](https://developer.android.com/google/play/billing/billing_overview) +For a list of steps for configuring in-app purchases in both stores, see the +[example app README](https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/in_app_purchase/example/README.md). -You can check out the [example app README](https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/example/README.md) for steps on how -to configure in app purchases in both stores. +Once you've configured your in-app purchases in their respective stores, you +can start using the plugin. Two basic options are available: -Once you've configured your in app purchases in their respective stores, you're -able to start using the plugin. There's two basic options available to you to -use. +1. A generic, idiomatic Flutter API: [in_app_purchase](https://pub.dev/documentation/in_app_purchase/latest/in_app_purchase/in_app_purchase-library.html). + This API supports most use cases for loading and making purchases. -1. [in_app_purchase.dart](https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/lib/src/in_app_purchase), - the generic idiomatic Flutter API. This exposes the most basic IAP-related - functionality. The goal is that Flutter apps should be able to use this API - surface on its own for the vast majority of cases. If you use this you should - be able to handle most use cases for loading and making purchases. If you would - like a more platform dependent approach, we also provide the second option as - below. +2. Platform-specific Dart APIs: [store_kit_wrappers](https://pub.dev/documentation/in_app_purchase/latest/store_kit_wrappers/store_kit_wrappers-library.html) + and [billing_client_wrappers](https://pub.dev/documentation/in_app_purchase/latest/billing_client_wrappers/billing_client_wrappers-library.html). + These APIs expose platform-specific behavior and allow for more fine-tuned + control when needed. However, if you use one of these APIs, your + purchase-handling logic is significantly different for the different + storefronts. -2. Dart APIs exposing the underlying platform APIs as directly as possible: - [store_kit_wrappers.dart](https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/lib/src/store_kit_wrappers) and - [billing_client_wrappers.dart](https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/lib/src/billing_client_wrappers). These - API surfaces should expose all the platform-specific behavior and allow for - more fine-tuned control when needed. However if you use this you'll need to - code your purchase handling logic significantly differently depending on - which platform you're on. +## Usage + +This section has examples of code for the following tasks: + +* [Initializing the plugin](#initializing-the-plugin) +* [Listening to purchase updates](#listening-to-purchase-updates) +* [Connecting to the underlying store](#connecting-to-the-underlying-store) +* [Loading products for sale](#loading-products-for-sale) +* [Loading previous purchases](#loading-previous-purchases) +* [Making a purchase](#making-a-purchase) +* [Completing a purchase](#completing-a-purchase) +* [Upgrading or downgrading an existing in-app subscription](#upgrading-or-downgrading-an-existing-in-app-subscription) +* [Presenting a code redemption sheet (iOS 14)](#presenting-a-code-redemption-sheet-ios-14) ### Initializing the plugin +The following initialization code is required for Google Play: + ```dart void main() { // Inform the plugin that this app supports pending purchases on Android. @@ -59,24 +77,32 @@ void main() { // // On iOS this is a no-op. InAppPurchaseConnection.enablePendingPurchases(); - runApp(MyApp()); } ``` +### Listening to purchase updates + +In your app's `initState` method, subscribe to any incoming purchases. These +can propagate from either underlying store. +You should always start listening to purchase update as early as possible to be able +to catch all purchase updates, including the ones from the previous app session. +To listen to the update: + ```dart -// Subscribe to any incoming purchases at app initialization. These can -// propagate from either storefront so it's important to listen as soon as -// possible to avoid losing events. class _MyAppState extends State { StreamSubscription> _subscription; @override void initState() { - final Stream purchaseUpdates = + final Stream purchaseUpdated = InAppPurchaseConnection.instance.purchaseUpdatedStream; - _subscription = purchaseUpdates.listen((purchases) { - _handlePurchaseUpdates(purchases); + _subscription = purchaseUpdated.listen((purchaseDetailsList) { + _listenToPurchaseUpdated(purchaseDetailsList); + }, onDone: () { + _subscription.cancel(); + }, onError: (error) { + // handle error here. }); super.initState(); } @@ -88,7 +114,35 @@ class _MyAppState extends State { } ``` -### Connecting to the Storefront +Here is an example of how to handle purchase updates: + +```dart +void _listenToPurchaseUpdated(List purchaseDetailsList) { + purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async { + if (purchaseDetails.status == PurchaseStatus.pending) { + _showPendingUI(); + } else { + if (purchaseDetails.status == PurchaseStatus.error) { + _handleError(purchaseDetails.error!); + } else if (purchaseDetails.status == PurchaseStatus.purchased) { + bool valid = await _verifyPurchase(purchaseDetails); + if (valid) { + _deliverProduct(purchaseDetails); + } else { + _handleInvalidPurchase(purchaseDetails); + return; + } + } + if (purchaseDetails.pendingCompletePurchase) { + await InAppPurchaseConnection.instance + .completePurchase(purchaseDetails); + } + } + }); +} +``` + +### Connecting to the underlying store ```dart final bool available = await InAppPurchaseConnection.instance.isAvailable(); @@ -100,30 +154,41 @@ if (!available) { ### Loading products for sale ```dart -// Set literals require Dart 2.2. Alternatively, use `Set _kIds = ['product1', 'product2'].toSet()`. -const Set _kIds = {'product1', 'product2'}; -final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds); +// Set literals require Dart 2.2. Alternatively, use +// `Set _kIds = ['product1', 'product2'].toSet()`. +const Set _kIds = {'product1', 'product2'}; +final ProductDetailsResponse response = + await InAppPurchaseConnection.instance.queryProductDetails(_kIds); if (response.notFoundIDs.isNotEmpty) { - // Handle the error. + // Handle the error. } List products = response.productDetails; ``` ### Loading previous purchases +In the following example, implement `_verifyPurchase` so that it verifies the +purchase following the best practices for each underlying store: + +* [Verifying App Store purchases](https://developer.apple.com/documentation/storekit/in-app_purchase/validating_receipts_with_the_app_store) +* [Verifying Google Play purchases](https://developer.android.com/google/play/billing/security#verify) + + ```dart -final QueryPurchaseDetailsResponse response = await InAppPurchaseConnection.instance.queryPastPurchases(); +final QueryPurchaseDetailsResponse response = + await InAppPurchaseConnection.instance.queryPastPurchases(); if (response.error != null) { - // Handle the error. + // Handle the error. } for (PurchaseDetails purchase in response.pastPurchases) { - _verifyPurchase(purchase); // Verify the purchase following the best practices for each storefront. - _deliverPurchase(purchase); // Deliver the purchase to the user in your app. - if (Platform.isIOS) { - // Mark that you've delivered the purchase. Only the App Store requires - // this final confirmation. - InAppPurchaseConnection.instance.completePurchase(purchase); - } + // Verify the purchase following best practices for each underlying store. + _verifyPurchase(purchase); + // Deliver the purchase to the user in your app. + _deliverPurchase(purchase); + if (purchase.pendingCompletePurchase) { + // Mark that you've delivered the purchase. This is mandatory. + InAppPurchaseConnection.instance.completePurchase(purchase); + } } ``` @@ -133,27 +198,9 @@ once they're marked as consumed and fails to return them here. For restoring these across devices you'll need to persist them on your own server and query that as well. -### Listening to purchase updates - -You should always start listening to purchase update as early as possible to be able -to catch all purchase updates, including the ones from the previous app session. -To listen to the update: - -```dart - Stream purchaseUpdated = - InAppPurchaseConnection.instance.purchaseUpdatedStream; - _subscription = purchaseUpdated.listen((purchaseDetailsList) { - _listenToPurchaseUpdated(purchaseDetailsList); - }, onDone: () { - _subscription.cancel(); - }, onError: (error) { - // handle error here. - }); -``` - ### Making a purchase -Both storefronts handle consumable and non-consumable products differently. If +Both underlying stores handle consumable and non-consumable products differently. If you're using `InAppPurchaseConnection`, you need to make a distinction here and call the right purchase method for each type. @@ -161,35 +208,39 @@ call the right purchase method for each type. final ProductDetails productDetails = ... // Saved earlier from queryPastPurchases(). final PurchaseParam purchaseParam = PurchaseParam(productDetails: productDetails); if (_isConsumable(productDetails)) { - InAppPurchaseConnection.instance.buyConsumable(purchaseParam: purchaseParam); + InAppPurchaseConnection.instance.buyConsumable(purchaseParam: purchaseParam); } else { - InAppPurchaseConnection.instance.buyNonConsumable(purchaseParam: purchaseParam); + InAppPurchaseConnection.instance.buyNonConsumable(purchaseParam: purchaseParam); } -// From here the purchase flow will be handled by the underlying storefront. +// From here the purchase flow will be handled by the underlying store. // Updates will be delivered to the `InAppPurchaseConnection.instance.purchaseUpdatedStream`. ``` -### Complete a purchase +### Completing a purchase The `InAppPurchaseConnection.purchaseUpdatedStream` will send purchase updates after you initiate the purchase flow using `InAppPurchaseConnection.buyConsumable` or `InAppPurchaseConnection.buyNonConsumable`. -After delivering the content to the user, you need to call `InAppPurchaseConnection.completePurchase` to tell the `GooglePlay` -and `AppStore` that the purchase has been finished. +After delivering the content to the user, call +`InAppPurchaseConnection.completePurchase` to tell the App Store and +Google Play that the purchase has been finished. -WARNING! Failure to call `InAppPurchaseConnection.completePurchase` and get a successful response within 3 days of the purchase will result a refund. +> **Warning:** Failure to call `InAppPurchaseConnection.completePurchase` and +> get a successful response within 3 days of the purchase will result a refund. -### Upgrading or Downgrading an existing InApp Subscription +### Upgrading or downgrading an existing in-app subscription -In order to upgrade/downgrade an existing InApp subscription on `PlayStore`, -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 `PurchaseParam` object while calling `InAppPurchaseConnection.buyNonConsumable`. -`AppStore` does not require this since they provides a subscription grouping mechanism. -Each subscription you offer must be assigned to a subscription group. -So the developers can group related subscriptions together to prevents users from -accidentally purchasing multiple subscriptions. -Please refer to the 'Creating a Subscription Group' sections of [Apple's subscription guide](https://developer.apple.com/app-store/subscriptions/) +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 `PurchaseParam` object while calling +`InAppPurchaseConnection.buyNonConsumable`. +The App Store does not require this because it provides a subscription +grouping mechanism. Each subscription you offer must be assigned to a +subscription group. Grouping related subscriptions together can help prevent +users from accidentally purchasing multiple subscriptions. Refer to the +[Creating a Subscription Group](https://developer.apple.com/app-store/subscriptions/#groups) section of +[Apple's subscription guide](https://developer.apple.com/app-store/subscriptions/). ```dart final PurchaseDetails oldPurchaseDetails = ...; @@ -202,7 +253,17 @@ InAppPurchaseConnection.instance .buyNonConsumable(purchaseParam: purchaseParam); ``` -## Development +### Presenting a code redemption sheet (iOS 14) + +The following code brings up a sheet that enables the user to redeem offer +codes that you've set up in App Store Connect. For more information on +redeeming offer codes, see [Implementing Offer Codes in Your App](https://developer.apple.com/documentation/storekit/in-app_purchase/subscriptions_and_offers/implementing_offer_codes_in_your_app). + +```dart +InAppPurchaseConnection.instance.presentCodeRedemptionSheet(); +``` + +## Contributing to this plugin This plugin uses [json_serializable](https://pub.dev/packages/json_serializable) for the @@ -211,3 +272,6 @@ editing any of the serialized data structs, rebuild the serializers by running `flutter packages pub run build_runner build --delete-conflicting-outputs`. `flutter packages pub run build_runner watch --delete-conflicting-outputs` will watch the filesystem for changes. + +If you would like to contribute to the plugin, check out our +[contribution guide](https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md). diff --git a/packages/in_app_purchase/in_app_purchase/doc/iap_android.gif b/packages/in_app_purchase/in_app_purchase/doc/iap_android.gif new file mode 100644 index 000000000000..86348e4f6294 Binary files /dev/null and b/packages/in_app_purchase/in_app_purchase/doc/iap_android.gif differ diff --git a/packages/in_app_purchase/in_app_purchase/doc/iap_ios.gif b/packages/in_app_purchase/in_app_purchase/doc/iap_ios.gif new file mode 100644 index 000000000000..a2cba74412d7 Binary files /dev/null and b/packages/in_app_purchase/in_app_purchase/doc/iap_ios.gif differ diff --git a/packages/in_app_purchase/in_app_purchase/example/README.md b/packages/in_app_purchase/in_app_purchase/example/README.md index 0ecf42298433..4140483cd1ca 100644 --- a/packages/in_app_purchase/in_app_purchase/example/README.md +++ b/packages/in_app_purchase/in_app_purchase/example/README.md @@ -4,8 +4,7 @@ Demonstrates how to use the In App Purchase (IAP) Plugin. ## Getting Started -This plugin is in beta. Please use with caution and file any potential issues -you see on our [issue tracker](https://github.com/flutter/flutter/issues/new/choose). +### Preparation There's a significant amount of setup required for testing in app purchases successfully, including registering new app IDs and store entries to use for diff --git a/packages/in_app_purchase/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/in_app_purchase/pubspec.yaml index b42ca8806819..5a0a3ba565ed 100644 --- a/packages/in_app_purchase/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase/pubspec.yaml @@ -1,7 +1,7 @@ name: in_app_purchase description: A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play. homepage: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase -version: 0.5.1+1 +version: 0.5.1+2 dependencies: flutter: