-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[in_app_purchase] Implementation of platform interface #3781
Changes from 1 commit
652e5d1
bcb9d38
23230c9
8f17a42
164700d
af8a388
a7693bd
b80a3c6
12943f4
7ec5de6
3ceef85
efc6120
e7f6216
24d305b
d86ac55
9eb358f
33d1118
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| ## 1.0.0 | ||
|
|
||
| - Initial open-source release. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| Copyright 2013 The Flutter Authors. All rights reserved. | ||
|
|
||
| Redistribution and use in source and binary forms, with or without modification, | ||
| are permitted provided that the following conditions are met: | ||
|
|
||
| * Redistributions of source code must retain the above copyright | ||
| notice, this list of conditions and the following disclaimer. | ||
| * Redistributions in binary form must reproduce the above | ||
| copyright notice, this list of conditions and the following | ||
| disclaimer in the documentation and/or other materials provided | ||
| with the distribution. | ||
| * Neither the name of Google Inc. nor the names of its | ||
| contributors may be used to endorse or promote products derived | ||
| from this software without specific prior written permission. | ||
|
|
||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
| ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
| WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
| DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
| ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
| (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
| LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
| ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
| (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
| SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # in_app_purchase_platform_interface | ||
|
|
||
| A common platform interface for the [`in_app_purchase`][1] plugin. | ||
|
|
||
| This interface allows platform-specific implementations of the `in_app_purchase` | ||
| plugin, as well as the plugin itself, to ensure they are supporting the | ||
| same interface. | ||
|
|
||
| # Usage | ||
|
|
||
| To implement a new platform-specific implementation of `in_app_purchase`, extend | ||
| [`InAppPurchasePlatform`][2] with an implementation that performs the | ||
| platform-specific behavior, and when you register your plugin, set the default | ||
| `InAppPurchasePlatform` by calling | ||
| `InAppPurchasePlatform.instance = MyPlatformInAppPurchase()`. | ||
cyanglaz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's also add doc for how to add additional features in a platform
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
| # Note on breaking changes | ||
|
|
||
| Strongly prefer non-breaking changes (such as adding a method to the interface) | ||
| over breaking changes for this package. | ||
|
|
||
| See https://flutter.dev/go/platform-interface-breaking-changes for a discussion | ||
| on why a less-clean interface is preferable to a breaking change. | ||
|
|
||
| [1]: ../in_app_purchase | ||
| [2]: lib/in_app_purchase_platform_interface.dart | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| // 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. | ||
|
|
||
| export 'src/in_app_purchase_platform.dart'; | ||
| export 'src/types/types.dart'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| // 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 'dart:async'; | ||
|
|
||
| import 'package:plugin_platform_interface/plugin_platform_interface.dart'; | ||
|
|
||
| import 'noop_in_app_purchase.dart'; | ||
| import 'types/types.dart'; | ||
|
|
||
| /// The interface that implementations of in_app_purchase must implement. | ||
| /// | ||
| /// Platform implementations should extend this class rather than implement it as `in_app_purchase` | ||
| /// does not consider newly added methods to be breaking changes. Extending this class | ||
| /// (using `extends`) ensures that the subclass will get the default implementation, while | ||
| /// platform implementations that `implements` this interface will be broken by newly added | ||
| /// [InAppPurchasePlatform] methods. | ||
| abstract class InAppPurchasePlatform extends PlatformInterface { | ||
| /// Constructs a UrlLauncherPlatform. | ||
| InAppPurchasePlatform() : super(token: _token); | ||
|
|
||
| static final Object _token = Object(); | ||
|
|
||
| static InAppPurchasePlatform _instance = NoopInAppPurchase(); | ||
|
|
||
| /// The default instance of [InAppPurchasePlatform] to use. | ||
| static InAppPurchasePlatform get instance => _instance; | ||
|
|
||
| /// Platform-specific plugins should set this with their own platform-specific | ||
| /// class that extends [InAppPurchasePlatform] when they register themselves. | ||
| // TODO(amirh): Extract common platform interface logic. | ||
| // https://github.com/flutter/flutter/issues/43368 | ||
| static set instance(InAppPurchasePlatform instance) { | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| PlatformInterface.verifyToken(instance, _token); | ||
| _instance = instance; | ||
| } | ||
|
|
||
| /// Listen to this broadcast stream to get real time update for purchases. | ||
| /// | ||
| /// This stream will never close as long as the app is active. | ||
| /// | ||
| /// Purchase updates can happen in several situations: | ||
| /// * When a purchase is triggered by user in the app. | ||
| /// * When a purchase is triggered by user from App Store or Google Play. | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// * If a purchase is not completed ([completePurchase] is not called on the | ||
| /// purchase object) from the last app session. Purchase updates will happen | ||
| /// when a new app session starts instead. | ||
| /// | ||
| /// IMPORTANT! You must subscribe to this stream as soon as your app launches, | ||
| /// preferably before returning your main App Widget in main(). Otherwise you | ||
| /// will miss purchase updated made before this stream is subscribed to. | ||
| /// | ||
| /// We also recommend listening to the stream with one subscription at a given | ||
| /// time. If you choose to have multiple subscription at the same time, you | ||
| /// should be careful at the fact that each subscription will receive all the | ||
| /// events after they start to listen. | ||
| Stream<List<PurchaseDetails>> get purchaseUpdatedStream => | ||
| throw UnimplementedError( | ||
| 'purchaseUpdatedStream has not been implemented.'); | ||
|
|
||
| /// Returns true if the payment platform is ready and available. | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Future<bool> isAvailable() => | ||
| throw UnimplementedError('isAvailable() has not been implemented.'); | ||
|
|
||
| /// Query product details for the given set of IDs. | ||
| /// | ||
| /// The [identifiers] need to exactly match existing configured product | ||
| /// identifiers in the underlying payment platform, whether that's [App Store | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// Connect](https://appstoreconnect.apple.com/) or [Google Play | ||
| /// Console](https://play.google.com/). | ||
| /// | ||
| /// See the [example readme](../../../../example/README.md) for steps on how | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// to initialize products on both payment platforms. | ||
| Future<ProductDetailsResponse> queryProductDetails(Set<String> identifiers) => | ||
| throw UnimplementedError( | ||
| 'queryProductDetails() had not been implemented.'); | ||
|
|
||
| /// Buy a non consumable product or subscription. | ||
| /// | ||
| /// Non consumable items can only be bought once. For example, a purchase that | ||
| /// unlocks a special content in your app. Subscriptions are also non | ||
| /// consumable products. | ||
| /// | ||
| /// You always need to restore all the non consumable products for user when | ||
| /// they switch their phones. | ||
| /// | ||
| /// This method does not return the result of the purchase. Instead, after | ||
| /// triggering this method, purchase updates will be sent to | ||
| /// [purchaseUpdatedStream]. You should [Stream.listen] to | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// [purchaseUpdatedStream] to get [PurchaseDetails] objects in different | ||
| /// [PurchaseDetails.status] and update your UI accordingly. When the | ||
| /// [PurchaseDetails.status] is [PurchaseStatus.purchased] or | ||
| /// [PurchaseStatus.error], you should deliver the content or handle the | ||
| /// error, then call [completePurchase] to finish the purchasing process. | ||
| /// | ||
| /// This method does return whether or not the purchase request was initially | ||
| /// sent successfully. | ||
| /// | ||
| /// Consumable items are defined differently by the different underlying | ||
| /// payment platforms, and there's no way to query for whether or not the | ||
| /// [ProductDetail] is a consumable at runtime. On iOS, products are defined | ||
| /// as non consumable items in the [App Store | ||
| /// Connect](https://appstoreconnect.apple.com/). [Google Play | ||
| /// Console](https://play.google.com/) products are considered consumable if | ||
| /// and when they are actively consumed manually. | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// | ||
| /// You can find more details on testing payments on iOS | ||
| /// [here](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/ShowUI.html#//apple_ref/doc/uid/TP40008267-CH3-SW11). | ||
| /// You can find more details on testing payments on Android | ||
| /// [here](https://developer.android.com/google/play/billing/billing_testing). | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// | ||
| /// See also: | ||
| /// | ||
| /// * [buyConsumable], for buying a consumable product. | ||
| /// * [queryPastPurchases], for restoring non consumable products. | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// | ||
| /// Calling this method for consumable items will cause unwanted behaviors! | ||
| Future<bool> buyNonConsumable({required PurchaseParam purchaseParam}) => | ||
| throw UnimplementedError('buyNonConsumable() has not been implemented.'); | ||
|
|
||
| /// Buy a consumable product. | ||
| /// | ||
| /// Consumable items can be "consumed" to mark that they've been used and then | ||
| /// bought additional times. For example, a health potion. | ||
| /// | ||
| /// To restore consumable purchases across devices, you should keep track of | ||
| /// those purchase on your own server and restore the purchase for your users. | ||
| /// Consumed products are no longer considered to be "owned" by payment | ||
| /// platforms and will not be delivered by calling [queryPastPurchases]. | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// | ||
| /// Consumable items are defined differently by the different underlying | ||
| /// payment platforms, and there's no way to query for whether or not the | ||
| /// [ProductDetail] is a consumable at runtime. On iOS, products are defined | ||
| /// as consumable items in the [App Store | ||
| /// Connect](https://appstoreconnect.apple.com/). [Google Play | ||
| /// Console](https://play.google.com/) products are considered consumable if | ||
| /// and when they are actively consumed manually. | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// | ||
| /// `autoConsume` is provided as a utility for Android only. It's meaningless | ||
|
||
| /// on iOS because the App Store automatically considers all potentially | ||
| /// consumable purchases "consumed" once the initial transaction is complete. | ||
| /// `autoConsume` is `true` by default, and we will call [consumePurchase] | ||
| /// after a successful purchase for you so that Google Play considers a | ||
| /// purchase consumed after the initial transaction, like iOS. If you'd like | ||
| /// to manually consume purchases in Play, you should set it to `false` and | ||
| /// manually call [consumePurchase] instead. Failing to consume a purchase | ||
| /// will cause user never be able to buy the same item again. Manually setting | ||
| /// this to `false` on iOS will throw an `Exception`. | ||
| /// | ||
| /// This method does not return the result of the purchase. Instead, after | ||
| /// triggering this method, purchase updates will be sent to | ||
| /// [purchaseUpdatedStream]. You should [Stream.listen] to | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// [purchaseUpdatedStream] to get [PurchaseDetails] objects in different | ||
| /// [PurchaseDetails.status] and update your UI accordingly. When the | ||
| /// [PurchaseDetails.status] is [PurchaseStatus.purchased] or | ||
| /// [PurchaseStatus.error], you should deliver the content or handle the | ||
| /// error, then call [completePurchase] to finish the purchasing process. | ||
| /// | ||
| /// This method does return whether or not the purchase request was initially | ||
| /// sent succesfully. | ||
| /// | ||
| /// See also: | ||
| /// | ||
| /// * [buyNonConsumable], for buying a non consumable product or | ||
| /// subscription. | ||
| /// * [queryPastPurchases], for restoring non consumable products. | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// * [consumePurchase], for manually consuming products on Android. | ||
|
||
| /// | ||
| /// Calling this method for non consumable items will cause unwanted | ||
| /// behaviors! | ||
| Future<bool> buyConsumable({ | ||
| required PurchaseParam purchaseParam, | ||
| bool autoConsume = true, | ||
| }) => | ||
| throw UnimplementedError('buyConsumable() has not been implemented.'); | ||
|
|
||
| // TODO(mvanbeusekom): Add definition for the `completePurchase` method. The | ||
| // current definition uses the Android specific `BillingResultWrapper` class | ||
| // which is not really platform generic and needs a solution. | ||
|
|
||
| /// Query all previous purchases. | ||
| /// | ||
| /// The `applicationUserName` should match whatever was sent in the initial | ||
| /// `PurchaseParam`, if anything. If no `applicationUserName` was specified in the initial | ||
| /// `PurchaseParam`, use `null`. | ||
| /// | ||
| /// This does not return consumed products. If you want to restore unused | ||
| /// consumable products, you need to persist consumable product information | ||
| /// for your user on your own server. | ||
| /// | ||
| /// See also: | ||
| /// | ||
| /// * [refreshPurchaseVerificationData], for reloading failed | ||
| /// [PurchaseDetails.verificationData]. | ||
| Future<QueryPurchaseDetailsResponse> queryPastPurchases( | ||
| {String? applicationUserName}) => | ||
| throw UnimplementedError('queryPastPurchase() has not been implemented.'); | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +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. | ||
|
|
||
| import 'in_app_purchase_platform.dart'; | ||
|
|
||
| class NoopInAppPurchase extends InAppPurchasePlatform {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| // 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 'in_app_purchase_source.dart'; | ||
|
|
||
| /// Captures an error from the underlying purchase platform. | ||
| /// | ||
| /// The error can happen during the purchase, restoring a purchase, or querying product. | ||
| /// Errors from restoring a purchase are not indicative of any errors during the original purchase. | ||
| /// See also: | ||
| /// * [ProductDetailsResponse] for error when querying product details. | ||
| /// * [PurchaseDetails] for error happened in purchase. | ||
| class IAPError { | ||
| /// Creates a new IAP error object with the given error details. | ||
| IAPError( | ||
| {required this.source, | ||
| required this.code, | ||
| required this.message, | ||
| this.details}); | ||
|
|
||
| /// Which source is the error on. | ||
| final IAPSource source; | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /// The error code. | ||
| final String code; | ||
|
|
||
| /// A human-readable error message. | ||
| final String message; | ||
|
|
||
| /// Error details, possibly null. | ||
| final dynamic details; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| // 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. | ||
|
|
||
| /// Which platform the request is on. | ||
| enum IAPSource { | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// Google's Play Store. | ||
| GooglePlay, | ||
|
|
||
| /// Apple's App Store. | ||
| AppStore | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| // 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. | ||
|
|
||
| /// The class represents the information of a product. | ||
| /// | ||
| /// This class unifies the BillingClient's [SkuDetailsWrapper] and StoreKit's [SKProductWrapper]. You can use the common attributes in | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// This class for simple operations. If you would like to see the detailed representation of the product, instead, use [skuDetails] on Android and [skProduct] on iOS. | ||
| class ProductDetails { | ||
| /// Creates a new product details object with the provided details. | ||
| ProductDetails({ | ||
| required this.id, | ||
| required this.title, | ||
| required this.description, | ||
| required this.price, | ||
| }); | ||
|
|
||
| /// The identifier of the product, specified in App Store Connect or Sku in Google Play console. | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| final String id; | ||
|
|
||
| /// The title of the product, specified in the App Store Connect or Sku in Google Play console based on the platform. | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| final String title; | ||
|
|
||
| /// The description of the product, specified in the App Store Connect or Sku in Google Play console based on the platform. | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| final String description; | ||
|
|
||
| /// The price of the product, specified in the App Store Connect or Sku in Google Play console based on the platform. | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// Formatted with currency symbol ("$0.99"). | ||
| final String price; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| // 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 'in_app_purchase_error.dart'; | ||
| import 'product_details.dart'; | ||
|
|
||
| /// The response returned by [InAppPurchaseConnection.queryProductDetails]. | ||
| /// | ||
| /// A list of [ProductDetails] can be obtained from the this response. | ||
| class ProductDetailsResponse { | ||
| /// Creates a new [ProductDetailsResponse] with the provided response details. | ||
| ProductDetailsResponse( | ||
| {required this.productDetails, required this.notFoundIDs, this.error}); | ||
|
|
||
| /// Each [ProductDetails] uniquely matches one valid identifier in [identifiers] of [InAppPurchaseConnection.queryProductDetails]. | ||
mvanbeusekom marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| final List<ProductDetails> productDetails; | ||
|
|
||
| /// The list of identifiers that are in the `identifiers` of [InAppPurchaseConnection.queryProductDetails] but failed to be fetched. | ||
| /// | ||
| /// There's multiple platform specific reasons that product information could fail to be fetched, | ||
| /// ranging from products not being correctly configured in the storefront to the queried IDs not existing. | ||
| final List<String> notFoundIDs; | ||
|
|
||
| /// A caught platform exception thrown while querying the purchases. | ||
| /// | ||
| /// The value is `null` if there is no error. | ||
| /// | ||
| /// It's possible for this to be null but for there still to be notFoundIds in cases where the request itself was a success but the | ||
| /// requested IDs could not be found. | ||
| final IAPError? error; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.