From 652e5d1b1d350013b4541296780c62fbc33a68fe Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 2 Apr 2021 09:11:26 +0200 Subject: [PATCH 01/17] Draft implementation of platform interface --- .../CHANGELOG.md | 3 + .../LICENSE | 25 +++ .../README.md | 26 +++ .../in_app_purchase_platform_interface.dart | 6 + .../lib/src/in_app_purchase_platform.dart | 199 ++++++++++++++++++ .../lib/src/noop_in_app_purchase.dart | 7 + .../lib/src/types/in_app_purchase_error.dart | 33 +++ .../lib/src/types/in_app_purchase_source.dart | 12 ++ .../lib/src/types/product_details.dart | 30 +++ .../src/types/product_details_response.dart | 32 +++ .../lib/src/types/purchase_details.dart | 61 ++++++ .../lib/src/types/purchase_param.dart | 27 +++ .../lib/src/types/purchase_status.dart | 23 ++ .../src/types/purchase_verification_data.dart | 48 +++++ .../query_purchase_details_response.dart | 26 +++ .../lib/src/types/types.dart | 13 ++ .../pubspec.yaml | 21 ++ .../test/in_app_purchase_platform_test.dart | 132 ++++++++++++ 18 files changed, 724 insertions(+) create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/LICENSE create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/README.md create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_error.dart create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_source.dart create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details.dart create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details_response.dart create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_details.dart create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_param.dart create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_verification_data.dart create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/query_purchase_details_response.dart create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..4558861a2bd5 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial open-source release. \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/LICENSE b/packages/in_app_purchase/in_app_purchase_platform_interface/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/LICENSE @@ -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. diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/README.md b/packages/in_app_purchase/in_app_purchase_platform_interface/README.md new file mode 100644 index 000000000000..a2a867c41fe3 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/README.md @@ -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()`. + +# 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 \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart new file mode 100644 index 000000000000..9263d497a4bf --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart @@ -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'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart new file mode 100644 index 000000000000..f6f2bd9c2616 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart @@ -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) { + 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. + /// * 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> get purchaseUpdatedStream => + throw UnimplementedError( + 'purchaseUpdatedStream has not been implemented.'); + + /// Returns true if the payment platform is ready and available. + Future 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 + /// Connect](https://appstoreconnect.apple.com/) or [Google Play + /// Console](https://play.google.com/). + /// + /// See the [example readme](../../../../example/README.md) for steps on how + /// to initialize products on both payment platforms. + Future queryProductDetails(Set 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 + /// [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. + /// + /// 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). + /// + /// See also: + /// + /// * [buyConsumable], for buying a consumable product. + /// * [queryPastPurchases], for restoring non consumable products. + /// + /// Calling this method for consumable items will cause unwanted behaviors! + Future 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]. + /// + /// 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. + /// + /// `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 + /// [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. + /// * [consumePurchase], for manually consuming products on Android. + /// + /// Calling this method for non consumable items will cause unwanted + /// behaviors! + Future 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 queryPastPurchases( + {String? applicationUserName}) => + throw UnimplementedError('queryPastPurchase() has not been implemented.'); +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart new file mode 100644 index 000000000000..6c9ef7309073 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart @@ -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 {} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_error.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_error.dart new file mode 100644 index 000000000000..45d043575a8f --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_error.dart @@ -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; + + /// The error code. + final String code; + + /// A human-readable error message. + final String message; + + /// Error details, possibly null. + final dynamic details; +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_source.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_source.dart new file mode 100644 index 000000000000..89ab546201ce --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_source.dart @@ -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 { + /// Google's Play Store. + GooglePlay, + + /// Apple's App Store. + AppStore +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details.dart new file mode 100644 index 000000000000..64a4c433ea78 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details.dart @@ -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 +/// 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. + final String id; + + /// The title of the product, specified in the App Store Connect or Sku in Google Play console based on the platform. + final String title; + + /// The description of the product, specified in the App Store Connect or Sku in Google Play console based on the platform. + final String description; + + /// The price of the product, specified in the App Store Connect or Sku in Google Play console based on the platform. + /// Formatted with currency symbol ("$0.99"). + final String price; +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details_response.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details_response.dart new file mode 100644 index 000000000000..437495a69cff --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details_response.dart @@ -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]. + final List 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 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; +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_details.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_details.dart new file mode 100644 index 000000000000..0b409017663e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_details.dart @@ -0,0 +1,61 @@ +// 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 'purchase_status.dart'; +import 'purchase_verification_data.dart'; + +/// Represents the transaction details of a purchase. +/// +/// This class unifies the BillingClient's [PurchaseWrapper] and StoreKit's [SKPaymentTransactionWrapper]. You can use the common attributes in +/// This class for simple operations. If you would like to see the detailed representation of the product, instead, use [PurchaseWrapper] on Android and [SKPaymentTransactionWrapper] on iOS. +class PurchaseDetails { + /// Creates a new PurchaseDetails object with the provided data. + PurchaseDetails({ + this.purchaseID, + required this.productID, + required this.verificationData, + required this.transactionDate, + }); + + /// A unique identifier of the purchase. + /// + /// The `value` is null on iOS if it is not a successful purchase. + final String? purchaseID; + + /// The product identifier of the purchase. + final String productID; + + /// The verification data of the purchase. + /// + /// Use this to verify the purchase. See [PurchaseVerificationData] for + /// details on how to verify purchase use this data. You should never use any + /// purchase data until verified. + /// + /// On iOS, [InAppPurchaseConnection.refreshPurchaseVerificationData] can be used to get a new + /// [PurchaseVerificationData] object for further validation. + final PurchaseVerificationData verificationData; + + /// The timestamp of the transaction. + /// + /// Milliseconds since epoch. + /// + /// The value is `null` if [status] is not [PurchaseStatus.purchased]. + final String? transactionDate; + + /// The status that this [PurchaseDetails] is currently on. + PurchaseStatus? status; + + /// The error details when the [status] is [PurchaseStatus.error]. + /// + /// The value is `null` if [status] is not [PurchaseStatus.error]. + IAPError? error; + + /// The developer has to call [InAppPurchaseConnection.completePurchase] if the value is `true` + /// and the product has been delivered to the user. + /// + /// The initial value is `false`. + /// * See also [InAppPurchaseConnection.completePurchase] for more details on completing purchases. + bool pendingCompletePurchase = false; +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_param.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_param.dart new file mode 100644 index 000000000000..cfe0ae44bfdb --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_param.dart @@ -0,0 +1,27 @@ +// 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 'product_details.dart'; + +/// The parameter object for generating a purchase. +class PurchaseParam { + /// Creates a new purchase parameter object with the given data. + PurchaseParam({ + required this.productDetails, + this.applicationUserName, + }); + + /// The product to create payment for. + /// + /// It has to match one of the valid [ProductDetails] objects that you get from [ProductDetailsResponse] after calling [InAppPurchaseConnection.queryProductDetails]. + final ProductDetails productDetails; + + /// An opaque id for the user's account that's unique to your app. (Optional) + /// + /// Used to help the store detect irregular activity. + /// Do not pass in a clear text, your developer ID, the user’s Apple ID, or the + /// user's Google ID for this field. + /// For example, you can use a one-way hash of the user’s account name on your server. + final String? applicationUserName; +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart new file mode 100644 index 000000000000..ef5fe45bfef1 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart @@ -0,0 +1,23 @@ +// 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. + +/// Status for a [PurchaseDetails]. +/// +/// This is the type for [PurchaseDetails.status]. +enum PurchaseStatus { + /// The purchase process is pending. + /// + /// You can update UI to let your users know the purchase is pending. + pending, + + /// The purchase is finished and successful. + /// + /// Update your UI to indicate the purchase is finished and deliver the product. + /// On Android, the google play store is handling the purchase, so we set the status to + /// `purchased` as long as we can successfully launch play store purchase flow. + purchased, + + /// Some error occurred in the purchase. The purchasing process if aborted. + error +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_verification_data.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_verification_data.dart new file mode 100644 index 000000000000..68d6ac7c4097 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_verification_data.dart @@ -0,0 +1,48 @@ +// 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'; + +/// Represents the data that is used to verify purchases. +/// +/// The property [source] helps you to determine the method to verify purchases. +/// Different source of purchase has different methods of verifying purchases. +/// +/// Both platforms have 2 ways to verify purchase data. You can either choose to verify the data locally using [localVerificationData] +/// or verify the data using your own server with [serverVerificationData]. +/// +/// For details on how to verify your purchase on iOS, +/// you can refer to Apple's document about [`About Receipt Validation`](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573-CH105-SW1). +/// +/// On Android, all purchase information should also be verified manually. See [`Verify a purchase`](https://developer.android.com/google/play/billing/billing_library_overview#Verify). +/// +/// It is preferable to verify purchases using a server with [serverVerificationData]. +/// +/// If the platform is iOS, it is possible the data can be null or your validation of this data turns out invalid. When this happens, +/// Call [InAppPurchaseConnection.refreshPurchaseVerificationData] to get a new [PurchaseVerificationData] object. And then you can +/// validate the receipt data again using one of the methods mentioned in [`Receipt Validation`](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573-CH105-SW1). +/// +/// You should never use any purchase data until verified. +class PurchaseVerificationData { + /// Creates a [PurchaseVerificationData] object with the provided information. + PurchaseVerificationData({ + required this.localVerificationData, + required this.serverVerificationData, + required this.source, + }); + + /// The data used for local verification. + /// + /// If the [source] is [IAPSource.AppStore], this data is a based64 encoded string. The structure of the payload is defined using ASN.1. + /// If the [source] is [IAPSource.GooglePlay], this data is a JSON String. + final String localVerificationData; + + /// The data used for server verification. + /// + /// If the platform is iOS, this data is identical to [localVerificationData]. + final String serverVerificationData; + + /// Indicates the source of the purchase. + final IAPSource source; +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/query_purchase_details_response.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/query_purchase_details_response.dart new file mode 100644 index 000000000000..4babc79266d7 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/query_purchase_details_response.dart @@ -0,0 +1,26 @@ +// 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 'purchase_details.dart'; + +/// The response object for fetching the past purchases. +/// +/// An instance of this class is returned in [InAppPurchaseConnection.queryPastPurchases]. +class QueryPurchaseDetailsResponse { + /// Creates a new [QueryPurchaseDetailsResponse] object with the provider information. + QueryPurchaseDetailsResponse({required this.pastPurchases, this.error}); + + /// A list of successfully fetched past purchases. + /// + /// If there are no past purchases, or there is an [error] fetching past purchases, + /// this variable is an empty List. + /// You should verify the purchase data using [PurchaseDetails.verificationData] before using the [PurchaseDetails] object. + final List pastPurchases; + + /// The error when fetching past purchases. + /// + /// If the fetch is successful, the value is `null`. + final IAPError? error; +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart new file mode 100644 index 000000000000..6e7e63f9563f --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart @@ -0,0 +1,13 @@ +// 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 'in_app_purchase_error.dart'; +export 'in_app_purchase_source.dart'; +export 'product_details.dart'; +export 'product_details_response.dart'; +export 'purchase_details.dart'; +export 'purchase_param.dart'; +export 'purchase_status.dart'; +export 'purchase_verification_data.dart'; +export 'query_purchase_details_response.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..91d1182d7c0a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml @@ -0,0 +1,21 @@ +name: in_app_purchase_platform_interface +description: A common platform interface for the in_app_purchase plugin. +homepage: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_platform_interface +# NOTE: We strongly prefer non-breaking changes, even at the expense of a +# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes +version: 1.0.0 + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^5.0.0-nullsafety.7 + pedantic: ^1.10.0 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.22.0" \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart new file mode 100644 index 000000000000..0b69b250f3b1 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart @@ -0,0 +1,132 @@ +// 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 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; +import 'package:in_app_purchase_platform_interface/src/noop_in_app_purchase.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$InAppPurchasePlatform', () { + test('$NoopInAppPurchase is the default instance', () { + expect(InAppPurchasePlatform.instance, isA()); + }); + + test('Cannot be implemented with `implements`', () { + expect(() { + InAppPurchasePlatform.instance = ImplementsInAppPurchasePlatform(); + }, throwsNoSuchMethodError); + }); + + test('Can be extended', () { + InAppPurchasePlatform.instance = ExtendsInAppPurchasePlatform(); + }); + + test('Can be mocked with `implements`', () { + final MockInAppPurchasePlatform mock = MockInAppPurchasePlatform(); + InAppPurchasePlatform.instance = mock; + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of purchaseUpdatedStream should throw unimplemented error', + () { + final ExtendsInAppPurchasePlatform inAppPurchasePlatform = + ExtendsInAppPurchasePlatform(); + + expect( + () => inAppPurchasePlatform.purchaseUpdatedStream, + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of isAvailable should throw unimplemented error', + () { + final ExtendsInAppPurchasePlatform inAppPurchasePlatform = + ExtendsInAppPurchasePlatform(); + + expect( + () => inAppPurchasePlatform.isAvailable(), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of queryProductDetails should throw unimplemented error', + () { + final ExtendsInAppPurchasePlatform inAppPurchasePlatform = + ExtendsInAppPurchasePlatform(); + + expect( + () => inAppPurchasePlatform.queryProductDetails({''}), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of buyNonConsumable should throw unimplemented error', + () { + final ExtendsInAppPurchasePlatform inAppPurchasePlatform = + ExtendsInAppPurchasePlatform(); + + expect( + () => inAppPurchasePlatform.buyNonConsumable( + purchaseParam: MockPurchaseParam(), + ), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of buyConsumable should throw unimplemented error', + () { + final ExtendsInAppPurchasePlatform inAppPurchasePlatform = + ExtendsInAppPurchasePlatform(); + + expect( + () => inAppPurchasePlatform.buyConsumable( + purchaseParam: MockPurchaseParam(), + ), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of queryPastPurchases should throw unimplemented error', + () { + final ExtendsInAppPurchasePlatform inAppPurchasePlatform = + ExtendsInAppPurchasePlatform(); + + expect( + () => inAppPurchasePlatform.queryPastPurchases(), + throwsUnimplementedError, + ); + }); + }); +} + +class ImplementsInAppPurchasePlatform implements InAppPurchasePlatform { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class MockInAppPurchasePlatform extends Mock + with + // ignore: prefer_mixin + MockPlatformInterfaceMixin + implements + InAppPurchasePlatform {} + +class ExtendsInAppPurchasePlatform extends InAppPurchasePlatform {} + +class MockPurchaseParam extends Mock implements PurchaseParam {} From bcb9d38771003e939f44d75451799a9662a0eaee Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 7 Apr 2021 11:06:00 +0200 Subject: [PATCH 02/17] Added finishPurchase and restorePurchase definitions --- .../lib/src/errors/purchase_exception.dart | 27 ++++++++++++ .../lib/src/in_app_purchase_platform.dart | 41 +++++++++++++++---- .../lib/src/noop_in_app_purchase.dart | 4 +- .../lib/src/types/purchase_status.dart | 9 +++- .../test/in_app_purchase_platform_test.dart | 23 +++++++++-- 5 files changed, 89 insertions(+), 15 deletions(-) create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/purchase_exception.dart diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/purchase_exception.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/purchase_exception.dart new file mode 100644 index 000000000000..ffa21f384eb5 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/purchase_exception.dart @@ -0,0 +1,27 @@ +import 'package:flutter/services.dart'; + +/// Thrown to indicate that a purchase could not be finished successfully. +/// +/// The exception should be implemented per platform. Each platform implementation +/// should override the [shouldRetry] property and return the correct value +/// according to the platform specific error codes. +abstract class PurchaseException implements Exception { + /// Creates a [PurchaseException] with the specified error [code] and optional + /// [message]. + PurchaseException({ + required this.code, + this.message, + }); + + /// The error code indicating + final String code; + + /// An human readible error message, possibly null. + final String? message; + + /// Indicates if the action should be retried or not. + /// + /// Implementing classes should override this property and make sure the + /// correct value is returned based on the [code] value. + bool get shouldRetry; +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart index f6f2bd9c2616..1671b278a3dc 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart @@ -43,6 +43,7 @@ abstract class InAppPurchasePlatform extends PlatformInterface { /// 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. + /// * When a purchase is restored on the device by the user in the app. /// * 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. @@ -55,9 +56,8 @@ abstract class InAppPurchasePlatform extends PlatformInterface { /// 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> get purchaseUpdatedStream => - throw UnimplementedError( - 'purchaseUpdatedStream has not been implemented.'); + Stream> get purchaseStream => + throw UnimplementedError('purchaseStream has not been implemented.'); /// Returns true if the payment platform is ready and available. Future isAvailable() => @@ -175,16 +175,40 @@ abstract class InAppPurchasePlatform extends PlatformInterface { }) => 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. + /// Mark that purchased content has been delivered to the user. + /// + /// You are responsible for completing every [PurchaseDetails] whose + /// [PurchaseDetails.status] is [PurchaseStatus.purchased]. Additionally on iOS, + /// the purchase needs to be completed if the [PurchaseDetails.status] is + /// [PurchaseStatus.error] or [PurchaseStatus.restored]. + /// Completing a [PurchaseStatus.pending] purchase will cause an exception. + /// For convenience, [PurchaseDetails.pendingCompletePurchase] indicates if a purchase is pending for completion. + /// + /// The method will throw a [PurchaseException] when the purchase could not be + /// finished. If the [PurchaseException.shouldRetry] is `true` the developer + /// should try to finish the purchase via this method again, or retry the [finishPurchase] + /// method at a later time. If the [PurchaseException.shouldRetry] is `false` + /// there might be some issue with the app's code or the configuration of the + /// app in the respective store. The developer is responsible to fix this issue. + /// The [PurchaseException.code] and [PurchaseException.message] fields might + /// provide more information on what went wrong. + /// + /// Warning! Failure to call this method and get a successful response within 3 days of the purchase will result a refund on Android. + /// The [consumePurchase] acts as an implicit [completePurchase] on Android. + Future finishPurchase(PurchaseDetails purchase) => + throw UnimplementedError('finishPurchase() has not been implemented.'); - /// Query all previous purchases. + /// Restore 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`. /// + /// Restored purchases are delivered through the [purchaseStream] with a + /// status of [PurchaseStatus.restored]. You should listen for these purchases, + /// validate their receipts, deliver the content and mark the purchase complete + /// by calling the [finishPurchase] method for each purchase. + /// /// 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. @@ -193,7 +217,6 @@ abstract class InAppPurchasePlatform extends PlatformInterface { /// /// * [refreshPurchaseVerificationData], for reloading failed /// [PurchaseDetails.verificationData]. - Future queryPastPurchases( - {String? applicationUserName}) => + Future restorePurchases({String? applicationUserName}) => throw UnimplementedError('queryPastPurchase() has not been implemented.'); } diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart index 6c9ef7309073..eb6cebcaa2ea 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:in_app_purchase_platform_interface/src/types/purchase_details.dart'; + import 'in_app_purchase_platform.dart'; -class NoopInAppPurchase extends InAppPurchasePlatform {} +class NoopInAppPurchase extends InAppPurchasePlatform { } diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart index ef5fe45bfef1..624908343c13 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart @@ -19,5 +19,12 @@ enum PurchaseStatus { purchased, /// Some error occurred in the purchase. The purchasing process if aborted. - error + error, + + /// The purchase has been restored to the device. + /// + /// You should validate the receipt and if valid deliver the content. Once the + /// content has been delivered or if the receipt is invalid you should finish + /// the purchase by calling the `finishPurchase` method. + restored, } diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart index 0b69b250f3b1..69b6484b7298 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart @@ -33,13 +33,13 @@ void main() { test( // ignore: lines_longer_than_80_chars - 'Default implementation of purchaseUpdatedStream should throw unimplemented error', + 'Default implementation of purchaseStream should throw unimplemented error', () { final ExtendsInAppPurchasePlatform inAppPurchasePlatform = ExtendsInAppPurchasePlatform(); expect( - () => inAppPurchasePlatform.purchaseUpdatedStream, + () => inAppPurchasePlatform.purchaseStream, throwsUnimplementedError, ); }); @@ -102,13 +102,26 @@ void main() { test( // ignore: lines_longer_than_80_chars - 'Default implementation of queryPastPurchases should throw unimplemented error', + 'Default implementation of finishPurchase should throw unimplemented error', () { final ExtendsInAppPurchasePlatform inAppPurchasePlatform = ExtendsInAppPurchasePlatform(); expect( - () => inAppPurchasePlatform.queryPastPurchases(), + () => inAppPurchasePlatform.finishPurchase(MockPurchaseDetails()), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of restorePurchases should throw unimplemented error', + () { + final ExtendsInAppPurchasePlatform inAppPurchasePlatform = + ExtendsInAppPurchasePlatform(); + + expect( + () => inAppPurchasePlatform.restorePurchases(), throwsUnimplementedError, ); }); @@ -130,3 +143,5 @@ class MockInAppPurchasePlatform extends Mock class ExtendsInAppPurchasePlatform extends InAppPurchasePlatform {} class MockPurchaseParam extends Mock implements PurchaseParam {} + +class MockPurchaseDetails extends Mock implements PurchaseDetails {} From 23230c9824c33cc49e72c6ab1e9c17a54eed7758 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 7 Apr 2021 11:33:41 +0200 Subject: [PATCH 03/17] Fix formatting and analysis warnings --- .../lib/in_app_purchase_platform_interface.dart | 1 + .../lib/src/errors/errors.dart | 1 + .../lib/src/errors/purchase_exception.dart | 14 ++++++++------ .../lib/src/noop_in_app_purchase.dart | 4 +--- 4 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart index 9263d497a4bf..c1a7ac69c09e 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart @@ -2,5 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'src/errors/errors.dart'; export 'src/in_app_purchase_platform.dart'; export 'src/types/types.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart new file mode 100644 index 000000000000..be9cd1527ec3 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart @@ -0,0 +1 @@ +export 'purchase_exception.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/purchase_exception.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/purchase_exception.dart index ffa21f384eb5..e050ef7f5b08 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/purchase_exception.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/purchase_exception.dart @@ -1,9 +1,11 @@ -import 'package:flutter/services.dart'; +// 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. /// Thrown to indicate that a purchase could not be finished successfully. -/// +/// /// The exception should be implemented per platform. Each platform implementation -/// should override the [shouldRetry] property and return the correct value +/// should override the [shouldRetry] property and return the correct value /// according to the platform specific error codes. abstract class PurchaseException implements Exception { /// Creates a [PurchaseException] with the specified error [code] and optional @@ -13,15 +15,15 @@ abstract class PurchaseException implements Exception { this.message, }); - /// The error code indicating + /// The error code indicating final String code; /// An human readible error message, possibly null. final String? message; /// Indicates if the action should be retried or not. - /// - /// Implementing classes should override this property and make sure the + /// + /// Implementing classes should override this property and make sure the /// correct value is returned based on the [code] value. bool get shouldRetry; } diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart index eb6cebcaa2ea..6c9ef7309073 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:in_app_purchase_platform_interface/src/types/purchase_details.dart'; - import 'in_app_purchase_platform.dart'; -class NoopInAppPurchase extends InAppPurchasePlatform { } +class NoopInAppPurchase extends InAppPurchasePlatform {} From 8f17a4229efc15220fed87304f0c4de3478deeb4 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 7 Apr 2021 11:39:35 +0200 Subject: [PATCH 04/17] Added missing license header --- .../lib/src/errors/errors.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart index be9cd1527ec3..38fe5b44195e 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart @@ -1 +1,5 @@ +// 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 'purchase_exception.dart'; From 164700dc2a4b3c2121ace53c47e733d87506ca07 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 7 Apr 2021 11:46:16 +0200 Subject: [PATCH 05/17] Fix analysis warnings --- .../lib/src/noop_in_app_purchase.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart index 6c9ef7309073..6808c07dae60 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart @@ -4,4 +4,7 @@ import 'in_app_purchase_platform.dart'; +/// Temporary no-operation implementation of the [InAppPurchasePlatform] which +/// was added to return as default implementation for the [InAppPurchasePlatform.instance] +/// property. class NoopInAppPurchase extends InAppPurchasePlatform {} From af8a388c57f9aabdb4af27bfa24812eae446c37f Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 7 Apr 2021 11:48:33 +0200 Subject: [PATCH 06/17] Fix typo --- .../lib/src/in_app_purchase_platform.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart index 1671b278a3dc..dd314b2951b5 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart @@ -17,7 +17,7 @@ import 'types/types.dart'; /// platform implementations that `implements` this interface will be broken by newly added /// [InAppPurchasePlatform] methods. abstract class InAppPurchasePlatform extends PlatformInterface { - /// Constructs a UrlLauncherPlatform. + /// Constructs a InAppPurchasePlatform. InAppPurchasePlatform() : super(token: _token); static final Object _token = Object(); From a7693bd9ade48245e05ad61ee99b31a6b77e6feb Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 7 Apr 2021 15:13:01 +0200 Subject: [PATCH 07/17] Remove NoopInAppPurchase implementation --- .../lib/src/in_app_purchase_platform.dart | 13 ++++++++++--- .../lib/src/noop_in_app_purchase.dart | 10 ---------- .../test/in_app_purchase_platform_test.dart | 5 ++--- 3 files changed, 12 insertions(+), 16 deletions(-) delete mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart index dd314b2951b5..dee78ee0f816 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart @@ -6,7 +6,6 @@ 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. @@ -22,10 +21,18 @@ abstract class InAppPurchasePlatform extends PlatformInterface { static final Object _token = Object(); - static InAppPurchasePlatform _instance = NoopInAppPurchase(); + static InAppPurchasePlatform? _instance; /// The default instance of [InAppPurchasePlatform] to use. - static InAppPurchasePlatform get instance => _instance; + static InAppPurchasePlatform get instance { + final InAppPurchasePlatform? platform = _instance; + if (platform == null) { + throw UnimplementedError( + 'No platform specific implementation set. Please make sure you set the `instance` with a valid platform specific implementation of the `InAppPurchasePlatform` class.'); + } + + return platform; + } /// Platform-specific plugins should set this with their own platform-specific /// class that extends [InAppPurchasePlatform] when they register themselves. diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart deleted file mode 100644 index 6808c07dae60..000000000000 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/noop_in_app_purchase.dart +++ /dev/null @@ -1,10 +0,0 @@ -// 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'; - -/// Temporary no-operation implementation of the [InAppPurchasePlatform] which -/// was added to return as default implementation for the [InAppPurchasePlatform.instance] -/// property. -class NoopInAppPurchase extends InAppPurchasePlatform {} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart index 69b6484b7298..9001a0612c46 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart @@ -4,7 +4,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; -import 'package:in_app_purchase_platform_interface/src/noop_in_app_purchase.dart'; import 'package:mockito/mockito.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; @@ -12,8 +11,8 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('$InAppPurchasePlatform', () { - test('$NoopInAppPurchase is the default instance', () { - expect(InAppPurchasePlatform.instance, isA()); + test('default instance is null and throws unimplemented exception', () { + expect(() => InAppPurchasePlatform.instance, throwsUnimplementedError); }); test('Cannot be implemented with `implements`', () { From b80a3c6b8cae664097578aacffa246fbf56f07d3 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 8 Apr 2021 10:14:31 +0200 Subject: [PATCH 08/17] Apply feedback from PR --- .../CHANGELOG.md | 2 +- .../lib/src/in_app_purchase_platform.dart | 25 +++++-------------- .../lib/src/types/in_app_purchase_error.dart | 4 +-- .../lib/src/types/in_app_purchase_source.dart | 12 --------- .../lib/src/types/purchase_status.dart | 5 ++-- .../src/types/purchase_verification_data.dart | 4 +-- .../lib/src/types/types.dart | 1 - .../pubspec.yaml | 2 +- .../test/in_app_purchase_platform_test.dart | 12 ++++----- 9 files changed, 18 insertions(+), 49 deletions(-) delete mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_source.dart diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md index 4558861a2bd5..2f529b31655d 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md @@ -1,3 +1,3 @@ ## 1.0.0 -- Initial open-source release. \ No newline at end of file +* Initial open-source release. \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart index dee78ee0f816..24f952b13ef4 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart @@ -21,26 +21,13 @@ abstract class InAppPurchasePlatform extends PlatformInterface { static final Object _token = Object(); - static InAppPurchasePlatform? _instance; - - /// The default instance of [InAppPurchasePlatform] to use. - static InAppPurchasePlatform get instance { - final InAppPurchasePlatform? platform = _instance; - if (platform == null) { - throw UnimplementedError( - 'No platform specific implementation set. Please make sure you set the `instance` with a valid platform specific implementation of the `InAppPurchasePlatform` class.'); - } - - return platform; - } - - /// 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) { + //// Ensures that implementers are using `extends` rather than + /// `implements` and throws [AssertionError] if not. + /// + /// This is implemented as a static method so that it cannot be overridden + /// with `noSuchMethod`. + static void verifyToken(InAppPurchasePlatform instance) { PlatformInterface.verifyToken(instance, _token); - _instance = instance; } /// Listen to this broadcast stream to get real time update for purchases. diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_error.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_error.dart index 45d043575a8f..f305f578f54a 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_error.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_error.dart @@ -2,8 +2,6 @@ // 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. @@ -20,7 +18,7 @@ class IAPError { this.details}); /// Which source is the error on. - final IAPSource source; + final String source; /// The error code. final String code; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_source.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_source.dart deleted file mode 100644 index 89ab546201ce..000000000000 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_source.dart +++ /dev/null @@ -1,12 +0,0 @@ -// 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 { - /// Google's Play Store. - GooglePlay, - - /// Apple's App Store. - AppStore -} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart index 624908343c13..c40951c6fa77 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart @@ -23,8 +23,9 @@ enum PurchaseStatus { /// The purchase has been restored to the device. /// - /// You should validate the receipt and if valid deliver the content. Once the + /// You should validate the purchase and if valid deliver the content. Once the /// content has been delivered or if the receipt is invalid you should finish - /// the purchase by calling the `finishPurchase` method. + /// the purchase by calling the `finishPurchase` method. More information on + /// verifying purchases can be found [here](https://pub.dev/packages/in_app_purchase#loading-previous-purchases). restored, } diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_verification_data.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_verification_data.dart index 68d6ac7c4097..3009758fc337 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_verification_data.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_verification_data.dart @@ -2,8 +2,6 @@ // 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'; - /// Represents the data that is used to verify purchases. /// /// The property [source] helps you to determine the method to verify purchases. @@ -44,5 +42,5 @@ class PurchaseVerificationData { final String serverVerificationData; /// Indicates the source of the purchase. - final IAPSource source; + final String source; } diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart index 6e7e63f9563f..14d7f67c2beb 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. export 'in_app_purchase_error.dart'; -export 'in_app_purchase_source.dart'; export 'product_details.dart'; export 'product_details_response.dart'; export 'purchase_details.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml index 91d1182d7c0a..9bea308463c9 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml @@ -18,4 +18,4 @@ dev_dependencies: environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.22.0" \ No newline at end of file + flutter: ">=1.22.0" diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart index 9001a0612c46..6d3367678ea6 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart @@ -11,23 +11,21 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('$InAppPurchasePlatform', () { - test('default instance is null and throws unimplemented exception', () { - expect(() => InAppPurchasePlatform.instance, throwsUnimplementedError); - }); - test('Cannot be implemented with `implements`', () { + final InAppPurchasePlatform instance = ImplementsInAppPurchasePlatform(); expect(() { - InAppPurchasePlatform.instance = ImplementsInAppPurchasePlatform(); + InAppPurchasePlatform.verifyToken(instance); }, throwsNoSuchMethodError); }); test('Can be extended', () { - InAppPurchasePlatform.instance = ExtendsInAppPurchasePlatform(); + final InAppPurchasePlatform instance = ExtendsInAppPurchasePlatform(); + InAppPurchasePlatform.verifyToken(instance); }); test('Can be mocked with `implements`', () { final MockInAppPurchasePlatform mock = MockInAppPurchasePlatform(); - InAppPurchasePlatform.instance = mock; + InAppPurchasePlatform.verifyToken(mock); }); test( From 12943f47829eed232270d73f4ab13d420c21112e Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 9 Apr 2021 14:27:13 +0200 Subject: [PATCH 09/17] Removed obsolete PurchaseException --- .../in_app_purchase_platform_interface.dart | 1 - .../lib/src/errors/errors.dart | 5 ---- .../lib/src/errors/purchase_exception.dart | 29 ------------------- 3 files changed, 35 deletions(-) delete mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart delete mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/purchase_exception.dart diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart index c1a7ac69c09e..9263d497a4bf 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart @@ -2,6 +2,5 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -export 'src/errors/errors.dart'; export 'src/in_app_purchase_platform.dart'; export 'src/types/types.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart deleted file mode 100644 index 38fe5b44195e..000000000000 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart +++ /dev/null @@ -1,5 +0,0 @@ -// 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 'purchase_exception.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/purchase_exception.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/purchase_exception.dart deleted file mode 100644 index e050ef7f5b08..000000000000 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/purchase_exception.dart +++ /dev/null @@ -1,29 +0,0 @@ -// 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. - -/// Thrown to indicate that a purchase could not be finished successfully. -/// -/// The exception should be implemented per platform. Each platform implementation -/// should override the [shouldRetry] property and return the correct value -/// according to the platform specific error codes. -abstract class PurchaseException implements Exception { - /// Creates a [PurchaseException] with the specified error [code] and optional - /// [message]. - PurchaseException({ - required this.code, - this.message, - }); - - /// The error code indicating - final String code; - - /// An human readible error message, possibly null. - final String? message; - - /// Indicates if the action should be retried or not. - /// - /// Implementing classes should override this property and make sure the - /// correct value is returned based on the [code] value. - bool get shouldRetry; -} From 7ec5de6d5a5bad8f536ce780d9733a2d89b9c82e Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 14 Apr 2021 15:29:46 +0200 Subject: [PATCH 10/17] Updated documentation per feedback --- .../lib/src/in_app_purchase_platform.dart | 103 +++++++----------- .../lib/src/types/product_details.dart | 20 ++-- .../src/types/product_details_response.dart | 6 +- .../lib/src/types/purchase_details.dart | 12 +- .../lib/src/types/purchase_status.dart | 4 +- .../src/types/purchase_verification_data.dart | 24 ++-- .../query_purchase_details_response.dart | 26 ----- .../lib/src/types/types.dart | 1 - .../test/in_app_purchase_platform_test.dart | 4 +- 9 files changed, 66 insertions(+), 134 deletions(-) delete mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/query_purchase_details_response.dart diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart index 24f952b13ef4..01f06111484e 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart @@ -36,7 +36,7 @@ abstract class InAppPurchasePlatform extends PlatformInterface { /// /// 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. + /// * When a purchase is triggered by user from the platform specific store front. /// * When a purchase is restored on the device by the user in the app. /// * If a purchase is not completed ([completePurchase] is not called on the /// purchase object) from the last app session. Purchase updates will happen @@ -53,19 +53,15 @@ abstract class InAppPurchasePlatform extends PlatformInterface { Stream> get purchaseStream => throw UnimplementedError('purchaseStream has not been implemented.'); - /// Returns true if the payment platform is ready and available. + /// Returns `true` if the payment platform is ready and available. Future 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 - /// Connect](https://appstoreconnect.apple.com/) or [Google Play - /// Console](https://play.google.com/). - /// - /// See the [example readme](../../../../example/README.md) for steps on how - /// to initialize products on both payment platforms. + /// Identifiers in the underlying payment platform, for example, [App Store + /// Connect](https://appstoreconnect.apple.com/) for iOS and [Google Play + /// Console](https://play.google.com/) for Android. Future queryProductDetails(Set identifiers) => throw UnimplementedError( 'queryProductDetails() had not been implemented.'); @@ -81,33 +77,24 @@ abstract class InAppPurchasePlatform extends PlatformInterface { /// /// 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 - /// [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. + /// [purchaseStream]. You should [Stream.listen] to [purchaseStream] to get + /// [PurchaseDetails] objects in different [PurchaseDetails.status] and update + /// your UI accordingly. When the [PurchaseDetails.status] is + /// [PurchaseStatus.purchased], [PurchaseStatus.restored] 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. - /// - /// 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). - /// + /// [ProductDetail] is a consumable at runtime. + /// /// See also: /// /// * [buyConsumable], for buying a consumable product. - /// * [queryPastPurchases], for restoring non consumable products. + /// * [restorePurchases], for restoring non consumable products. /// /// Calling this method for consumable items will cause unwanted behaviors! Future buyNonConsumable({required PurchaseParam purchaseParam}) => @@ -121,31 +108,21 @@ abstract class InAppPurchasePlatform extends PlatformInterface { /// 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]. + /// platforms and will not be delivered by calling [restorePurchases]. /// /// 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. - /// - /// `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`. + /// [ProductDetail] is a consumable at runtime. + /// + /// `autoConsume` is provided as a utility and will instruct the plugin to + /// automatically consume the product after a succesful purchase. + /// `autoConsume` is `true` by default. On iOS comsumable products are + /// consumed automatically by the App Store and this parameter is ignored. /// /// 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 - /// [purchaseUpdatedStream] to get [PurchaseDetails] objects in different + /// [purchaseStream]. You should [Stream.listen] to + /// [purchaseStream] 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 @@ -158,7 +135,7 @@ abstract class InAppPurchasePlatform extends PlatformInterface { /// /// * [buyNonConsumable], for buying a non consumable product or /// subscription. - /// * [queryPastPurchases], for restoring non consumable products. + /// * [restorePurchases], for restoring non consumable products. /// * [consumePurchase], for manually consuming products on Android. /// /// Calling this method for non consumable items will cause unwanted @@ -172,25 +149,23 @@ abstract class InAppPurchasePlatform extends PlatformInterface { /// Mark that purchased content has been delivered to the user. /// /// You are responsible for completing every [PurchaseDetails] whose - /// [PurchaseDetails.status] is [PurchaseStatus.purchased]. Additionally on iOS, - /// the purchase needs to be completed if the [PurchaseDetails.status] is - /// [PurchaseStatus.error] or [PurchaseStatus.restored]. + /// [PurchaseDetails.status] is [PurchaseStatus.purchased] or + /// [PurchaseStatus.restored]. /// Completing a [PurchaseStatus.pending] purchase will cause an exception. - /// For convenience, [PurchaseDetails.pendingCompletePurchase] indicates if a purchase is pending for completion. + /// For convenience, [PurchaseDetails.pendingCompletePurchase] indicates if a + /// purchase is pending for completion. /// /// The method will throw a [PurchaseException] when the purchase could not be - /// finished. If the [PurchaseException.shouldRetry] is `true` the developer - /// should try to finish the purchase via this method again, or retry the [finishPurchase] - /// method at a later time. If the [PurchaseException.shouldRetry] is `false` - /// there might be some issue with the app's code or the configuration of the - /// app in the respective store. The developer is responsible to fix this issue. - /// The [PurchaseException.code] and [PurchaseException.message] fields might - /// provide more information on what went wrong. - /// - /// Warning! Failure to call this method and get a successful response within 3 days of the purchase will result a refund on Android. - /// The [consumePurchase] acts as an implicit [completePurchase] on Android. - Future finishPurchase(PurchaseDetails purchase) => - throw UnimplementedError('finishPurchase() has not been implemented.'); + /// finished. Depending on the [PurchaseException.errorCode] the developer + /// should try to complete the purchase via this method again, or retry the + /// [completePurchase] method at a later time. If the + /// [PurchaseException.errorCode] indicates you should not retry there might + /// be some issue with the app's code or the configuration of the app in the + /// respective store. The developer is responsible to fix this issue. The + /// [PurchaseException.message] field might provide more information on what + /// went wrong. + Future completePurchase(PurchaseDetails purchase) => + throw UnimplementedError('completePurchase() has not been implemented.'); /// Restore all previous purchases. /// @@ -212,5 +187,5 @@ abstract class InAppPurchasePlatform extends PlatformInterface { /// * [refreshPurchaseVerificationData], for reloading failed /// [PurchaseDetails.verificationData]. Future restorePurchases({String? applicationUserName}) => - throw UnimplementedError('queryPastPurchase() has not been implemented.'); + throw UnimplementedError('restorePurchases() has not been implemented.'); } diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details.dart index 64a4c433ea78..e1e563d6f905 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details.dart @@ -3,9 +3,6 @@ // 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 -/// 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({ @@ -15,16 +12,23 @@ class ProductDetails { required this.price, }); - /// The identifier of the product, specified in App Store Connect or Sku in Google Play console. + /// The identifier of the product. + /// + /// For example, on iOS it is specified in App Store Connect; on Android, it is specified in Google Play Console. final String id; - /// The title of the product, specified in the App Store Connect or Sku in Google Play console based on the platform. + /// The title of the product. + /// + /// For example, on iOS it is specified in App Store Connect; on Android, it is specified in Google Play Console. final String title; - /// The description of the product, specified in the App Store Connect or Sku in Google Play console based on the platform. + /// The description of the product. + /// + /// For example, on iOS it is specified in App Store Connect; on Android, it is specified in Google Play Console. final String description; - /// The price of the product, specified in the App Store Connect or Sku in Google Play console based on the platform. - /// Formatted with currency symbol ("$0.99"). + /// The price of the product, formatted with currency symbol ("$0.99"). + /// + /// For example, on iOS it is specified in App Store Connect; on Android, it is specified in Google Play Console. final String price; } diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details_response.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details_response.dart index 437495a69cff..868f9428add2 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details_response.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details_response.dart @@ -5,7 +5,7 @@ import 'in_app_purchase_error.dart'; import 'product_details.dart'; -/// The response returned by [InAppPurchaseConnection.queryProductDetails]. +/// The response returned by [InAppPurchasePlatform.queryProductDetails]. /// /// A list of [ProductDetails] can be obtained from the this response. class ProductDetailsResponse { @@ -13,10 +13,10 @@ class ProductDetailsResponse { ProductDetailsResponse( {required this.productDetails, required this.notFoundIDs, this.error}); - /// Each [ProductDetails] uniquely matches one valid identifier in [identifiers] of [InAppPurchaseConnection.queryProductDetails]. + /// Each [ProductDetails] uniquely matches one valid identifier in [identifiers] of [InAppPurchasePlatform.queryProductDetails]. final List productDetails; - /// The list of identifiers that are in the `identifiers` of [InAppPurchaseConnection.queryProductDetails] but failed to be fetched. + /// The list of identifiers that are in the `identifiers` of [InAppPurchasePlatform.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. diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_details.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_details.dart index 0b409017663e..2638e306aa97 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_details.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_details.dart @@ -7,9 +7,6 @@ import 'purchase_status.dart'; import 'purchase_verification_data.dart'; /// Represents the transaction details of a purchase. -/// -/// This class unifies the BillingClient's [PurchaseWrapper] and StoreKit's [SKPaymentTransactionWrapper]. You can use the common attributes in -/// This class for simple operations. If you would like to see the detailed representation of the product, instead, use [PurchaseWrapper] on Android and [SKPaymentTransactionWrapper] on iOS. class PurchaseDetails { /// Creates a new PurchaseDetails object with the provided data. PurchaseDetails({ @@ -20,8 +17,6 @@ class PurchaseDetails { }); /// A unique identifier of the purchase. - /// - /// The `value` is null on iOS if it is not a successful purchase. final String? purchaseID; /// The product identifier of the purchase. @@ -32,9 +27,6 @@ class PurchaseDetails { /// Use this to verify the purchase. See [PurchaseVerificationData] for /// details on how to verify purchase use this data. You should never use any /// purchase data until verified. - /// - /// On iOS, [InAppPurchaseConnection.refreshPurchaseVerificationData] can be used to get a new - /// [PurchaseVerificationData] object for further validation. final PurchaseVerificationData verificationData; /// The timestamp of the transaction. @@ -52,10 +44,10 @@ class PurchaseDetails { /// The value is `null` if [status] is not [PurchaseStatus.error]. IAPError? error; - /// The developer has to call [InAppPurchaseConnection.completePurchase] if the value is `true` + /// The developer has to call [InAppPurchasePlatform.completePurchase] if the value is `true` /// and the product has been delivered to the user. /// /// The initial value is `false`. - /// * See also [InAppPurchaseConnection.completePurchase] for more details on completing purchases. + /// * See also [InAppPurchasePlatform.completePurchase] for more details on completing purchases. bool pendingCompletePurchase = false; } diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart index c40951c6fa77..69f31c8f0641 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart @@ -14,8 +14,6 @@ enum PurchaseStatus { /// The purchase is finished and successful. /// /// Update your UI to indicate the purchase is finished and deliver the product. - /// On Android, the google play store is handling the purchase, so we set the status to - /// `purchased` as long as we can successfully launch play store purchase flow. purchased, /// Some error occurred in the purchase. The purchasing process if aborted. @@ -25,7 +23,7 @@ enum PurchaseStatus { /// /// You should validate the purchase and if valid deliver the content. Once the /// content has been delivered or if the receipt is invalid you should finish - /// the purchase by calling the `finishPurchase` method. More information on + /// the purchase by calling the `completePurchase` method. More information on /// verifying purchases can be found [here](https://pub.dev/packages/in_app_purchase#loading-previous-purchases). restored, } diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_verification_data.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_verification_data.dart index 3009758fc337..d3dfa3ad3bbd 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_verification_data.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_verification_data.dart @@ -7,19 +7,10 @@ /// The property [source] helps you to determine the method to verify purchases. /// Different source of purchase has different methods of verifying purchases. /// -/// Both platforms have 2 ways to verify purchase data. You can either choose to verify the data locally using [localVerificationData] -/// or verify the data using your own server with [serverVerificationData]. -/// -/// For details on how to verify your purchase on iOS, -/// you can refer to Apple's document about [`About Receipt Validation`](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573-CH105-SW1). -/// -/// On Android, all purchase information should also be verified manually. See [`Verify a purchase`](https://developer.android.com/google/play/billing/billing_library_overview#Verify). -/// -/// It is preferable to verify purchases using a server with [serverVerificationData]. -/// -/// If the platform is iOS, it is possible the data can be null or your validation of this data turns out invalid. When this happens, -/// Call [InAppPurchaseConnection.refreshPurchaseVerificationData] to get a new [PurchaseVerificationData] object. And then you can -/// validate the receipt data again using one of the methods mentioned in [`Receipt Validation`](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573-CH105-SW1). +/// Both platforms have 2 ways to verify purchase data. You can either choose to +/// verify the data locally using [localVerificationData] or verify the data +/// using your own server with [serverVerificationData]. It is preferable to +/// verify purchases using a server with [serverVerificationData]. /// /// You should never use any purchase data until verified. class PurchaseVerificationData { @@ -32,13 +23,12 @@ class PurchaseVerificationData { /// The data used for local verification. /// - /// If the [source] is [IAPSource.AppStore], this data is a based64 encoded string. The structure of the payload is defined using ASN.1. - /// If the [source] is [IAPSource.GooglePlay], this data is a JSON String. + /// The data is formatted according to the specifications of the respective + /// store. You can use the [source] field to determine the store from which + /// the data originated and proces the data accordingly. final String localVerificationData; /// The data used for server verification. - /// - /// If the platform is iOS, this data is identical to [localVerificationData]. final String serverVerificationData; /// Indicates the source of the purchase. diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/query_purchase_details_response.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/query_purchase_details_response.dart deleted file mode 100644 index 4babc79266d7..000000000000 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/query_purchase_details_response.dart +++ /dev/null @@ -1,26 +0,0 @@ -// 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 'purchase_details.dart'; - -/// The response object for fetching the past purchases. -/// -/// An instance of this class is returned in [InAppPurchaseConnection.queryPastPurchases]. -class QueryPurchaseDetailsResponse { - /// Creates a new [QueryPurchaseDetailsResponse] object with the provider information. - QueryPurchaseDetailsResponse({required this.pastPurchases, this.error}); - - /// A list of successfully fetched past purchases. - /// - /// If there are no past purchases, or there is an [error] fetching past purchases, - /// this variable is an empty List. - /// You should verify the purchase data using [PurchaseDetails.verificationData] before using the [PurchaseDetails] object. - final List pastPurchases; - - /// The error when fetching past purchases. - /// - /// If the fetch is successful, the value is `null`. - final IAPError? error; -} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart index 14d7f67c2beb..33d183c51d04 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart @@ -9,4 +9,3 @@ export 'purchase_details.dart'; export 'purchase_param.dart'; export 'purchase_status.dart'; export 'purchase_verification_data.dart'; -export 'query_purchase_details_response.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart index 6d3367678ea6..9e131bb6c6c8 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart @@ -99,13 +99,13 @@ void main() { test( // ignore: lines_longer_than_80_chars - 'Default implementation of finishPurchase should throw unimplemented error', + 'Default implementation of completePurchase should throw unimplemented error', () { final ExtendsInAppPurchasePlatform inAppPurchasePlatform = ExtendsInAppPurchasePlatform(); expect( - () => inAppPurchasePlatform.finishPurchase(MockPurchaseDetails()), + () => inAppPurchasePlatform.completePurchase(MockPurchaseDetails()), throwsUnimplementedError, ); }); From 3ceef85efba421bfaae32833333e39047dea0722 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 14 Apr 2021 15:41:52 +0200 Subject: [PATCH 11/17] Fixed formatting --- .../lib/src/in_app_purchase_platform.dart | 38 +++++++++---------- .../src/types/purchase_verification_data.dart | 4 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart index 01f06111484e..46aba266aa48 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart @@ -59,9 +59,9 @@ abstract class InAppPurchasePlatform extends PlatformInterface { /// Query product details for the given set of IDs. /// - /// Identifiers in the underlying payment platform, for example, [App Store - /// Connect](https://appstoreconnect.apple.com/) for iOS and [Google Play - /// Console](https://play.google.com/) for Android. + /// Identifiers in the underlying payment platform, for example, [App Store + /// Connect](https://appstoreconnect.apple.com/) for iOS and [Google Play + /// Console](https://play.google.com/) for Android. Future queryProductDetails(Set identifiers) => throw UnimplementedError( 'queryProductDetails() had not been implemented.'); @@ -77,10 +77,10 @@ abstract class InAppPurchasePlatform extends PlatformInterface { /// /// This method does not return the result of the purchase. Instead, after /// triggering this method, purchase updates will be sent to - /// [purchaseStream]. You should [Stream.listen] to [purchaseStream] to get + /// [purchaseStream]. You should [Stream.listen] to [purchaseStream] to get /// [PurchaseDetails] objects in different [PurchaseDetails.status] and update - /// your UI accordingly. When the [PurchaseDetails.status] is - /// [PurchaseStatus.purchased], [PurchaseStatus.restored] or + /// your UI accordingly. When the [PurchaseDetails.status] is + /// [PurchaseStatus.purchased], [PurchaseStatus.restored] or /// [PurchaseStatus.error] you should deliver the content or handle the error, /// then call [completePurchase] to finish the purchasing process. /// @@ -90,7 +90,7 @@ abstract class InAppPurchasePlatform extends PlatformInterface { /// 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. - /// + /// /// See also: /// /// * [buyConsumable], for buying a consumable product. @@ -114,10 +114,10 @@ abstract class InAppPurchasePlatform extends PlatformInterface { /// payment platforms, and there's no way to query for whether or not the /// [ProductDetail] is a consumable at runtime. /// - /// `autoConsume` is provided as a utility and will instruct the plugin to - /// automatically consume the product after a succesful purchase. - /// `autoConsume` is `true` by default. On iOS comsumable products are - /// consumed automatically by the App Store and this parameter is ignored. + /// `autoConsume` is provided as a utility and will instruct the plugin to + /// automatically consume the product after a succesful purchase. + /// `autoConsume` is `true` by default. On iOS comsumable products are + /// consumed automatically by the App Store and this parameter is ignored. /// /// This method does not return the result of the purchase. Instead, after /// triggering this method, purchase updates will be sent to @@ -149,20 +149,20 @@ abstract class InAppPurchasePlatform extends PlatformInterface { /// Mark that purchased content has been delivered to the user. /// /// You are responsible for completing every [PurchaseDetails] whose - /// [PurchaseDetails.status] is [PurchaseStatus.purchased] or + /// [PurchaseDetails.status] is [PurchaseStatus.purchased] or /// [PurchaseStatus.restored]. /// Completing a [PurchaseStatus.pending] purchase will cause an exception. - /// For convenience, [PurchaseDetails.pendingCompletePurchase] indicates if a + /// For convenience, [PurchaseDetails.pendingCompletePurchase] indicates if a /// purchase is pending for completion. /// /// The method will throw a [PurchaseException] when the purchase could not be /// finished. Depending on the [PurchaseException.errorCode] the developer - /// should try to complete the purchase via this method again, or retry the - /// [completePurchase] method at a later time. If the - /// [PurchaseException.errorCode] indicates you should not retry there might - /// be some issue with the app's code or the configuration of the app in the - /// respective store. The developer is responsible to fix this issue. The - /// [PurchaseException.message] field might provide more information on what + /// should try to complete the purchase via this method again, or retry the + /// [completePurchase] method at a later time. If the + /// [PurchaseException.errorCode] indicates you should not retry there might + /// be some issue with the app's code or the configuration of the app in the + /// respective store. The developer is responsible to fix this issue. The + /// [PurchaseException.message] field might provide more information on what /// went wrong. Future completePurchase(PurchaseDetails purchase) => throw UnimplementedError('completePurchase() has not been implemented.'); diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_verification_data.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_verification_data.dart index d3dfa3ad3bbd..49f2a7539d62 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_verification_data.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_verification_data.dart @@ -8,8 +8,8 @@ /// Different source of purchase has different methods of verifying purchases. /// /// Both platforms have 2 ways to verify purchase data. You can either choose to -/// verify the data locally using [localVerificationData] or verify the data -/// using your own server with [serverVerificationData]. It is preferable to +/// verify the data locally using [localVerificationData] or verify the data +/// using your own server with [serverVerificationData]. It is preferable to /// verify purchases using a server with [serverVerificationData]. /// /// You should never use any purchase data until verified. From efc6120564848e75de4aaf68fd75693368f54af5 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 14 Apr 2021 19:56:05 +0200 Subject: [PATCH 12/17] Allow nullable instance --- .../lib/src/in_app_purchase_platform.dart | 17 ++++++++++++----- .../test/in_app_purchase_platform_test.dart | 13 +++++++------ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart index 46aba266aa48..7d482d7c2762 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart @@ -21,13 +21,20 @@ abstract class InAppPurchasePlatform extends PlatformInterface { static final Object _token = Object(); - //// Ensures that implementers are using `extends` rather than - /// `implements` and throws [AssertionError] if not. + /// The default instance of [InAppPurchasePlatform] to use. /// - /// This is implemented as a static method so that it cannot be overridden - /// with `noSuchMethod`. - static void verifyToken(InAppPurchasePlatform instance) { + /// Defaults to `null`. + static InAppPurchasePlatform? get instance => _instance; + + static InAppPurchasePlatform? _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 void setInstance(InAppPurchasePlatform instance) { PlatformInterface.verifyToken(instance, _token); + _instance = instance; } /// Listen to this broadcast stream to get real time update for purchases. diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart index 9e131bb6c6c8..d5c1ae5fc127 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart @@ -11,21 +11,22 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('$InAppPurchasePlatform', () { + test('Default instance should return null', () { + expect(InAppPurchasePlatform.instance, null); + }); + test('Cannot be implemented with `implements`', () { - final InAppPurchasePlatform instance = ImplementsInAppPurchasePlatform(); expect(() { - InAppPurchasePlatform.verifyToken(instance); + InAppPurchasePlatform.setInstance(ImplementsInAppPurchasePlatform()); }, throwsNoSuchMethodError); }); test('Can be extended', () { - final InAppPurchasePlatform instance = ExtendsInAppPurchasePlatform(); - InAppPurchasePlatform.verifyToken(instance); + InAppPurchasePlatform.setInstance(ExtendsInAppPurchasePlatform()); }); test('Can be mocked with `implements`', () { - final MockInAppPurchasePlatform mock = MockInAppPurchasePlatform(); - InAppPurchasePlatform.verifyToken(mock); + InAppPurchasePlatform.setInstance(MockInAppPurchasePlatform()); }); test( From e7f6216cee1d8fcba1e52319b4bc94d1ec4cf36a Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 14 Apr 2021 20:00:14 +0200 Subject: [PATCH 13/17] Updated readme to reflect setInstance method --- .../in_app_purchase_platform_interface/README.md | 2 +- .../lib/src/in_app_purchase_platform.dart | 2 +- .../lib/src/types/purchase_param.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/README.md b/packages/in_app_purchase/in_app_purchase_platform_interface/README.md index a2a867c41fe3..9dc71b566f1f 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/README.md +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/README.md @@ -12,7 +12,7 @@ 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()`. +`InAppPurchasePlatform.setInstance(MyPlatformInAppPurchase())`. # Note on breaking changes diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart index 7d482d7c2762..2d96c02fb5fa 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart @@ -21,7 +21,7 @@ abstract class InAppPurchasePlatform extends PlatformInterface { static final Object _token = Object(); - /// The default instance of [InAppPurchasePlatform] to use. + /// The instance of [InAppPurchasePlatform] to use. /// /// Defaults to `null`. static InAppPurchasePlatform? get instance => _instance; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_param.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_param.dart index cfe0ae44bfdb..df75159c152b 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_param.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_param.dart @@ -14,7 +14,7 @@ class PurchaseParam { /// The product to create payment for. /// - /// It has to match one of the valid [ProductDetails] objects that you get from [ProductDetailsResponse] after calling [InAppPurchaseConnection.queryProductDetails]. + /// It has to match one of the valid [ProductDetails] objects that you get from [ProductDetailsResponse] after calling [InAppPurchasePlatform.queryProductDetails]. final ProductDetails productDetails; /// An opaque id for the user's account that's unique to your app. (Optional) From 24d305b1b14d73756aa1dd5f16c631fbb11d15c7 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 21 Apr 2021 17:39:49 +0200 Subject: [PATCH 14/17] Removed platform specific comments --- .../lib/src/in_app_purchase_platform.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart index 2d96c02fb5fa..f8dc4c998494 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart @@ -123,8 +123,7 @@ abstract class InAppPurchasePlatform extends PlatformInterface { /// /// `autoConsume` is provided as a utility and will instruct the plugin to /// automatically consume the product after a succesful purchase. - /// `autoConsume` is `true` by default. On iOS comsumable products are - /// consumed automatically by the App Store and this parameter is ignored. + /// `autoConsume` is `true` by default. /// /// This method does not return the result of the purchase. Instead, after /// triggering this method, purchase updates will be sent to @@ -143,7 +142,6 @@ abstract class InAppPurchasePlatform extends PlatformInterface { /// * [buyNonConsumable], for buying a non consumable product or /// subscription. /// * [restorePurchases], for restoring non consumable products. - /// * [consumePurchase], for manually consuming products on Android. /// /// Calling this method for non consumable items will cause unwanted /// behaviors! From d86ac5589c675332ef896c2429c1c1f5ac83d36d Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 21 Apr 2021 21:47:07 +0200 Subject: [PATCH 15/17] Add interfaces to support InAppPurchaseAddition --- .../lib/in_app_purchase_platform_interface.dart | 2 ++ .../lib/src/in_app_purchase_addition.dart | 11 +++++++++++ .../src/in_app_purchase_addition_provider.dart | 17 +++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_addition.dart create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_addition_provider.dart diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart index 9263d497a4bf..879bca786781 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart @@ -2,5 +2,7 @@ // 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_addition.dart'; +export 'src/in_app_purchase_addition_provider.dart'; export 'src/in_app_purchase_platform.dart'; export 'src/types/types.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_addition.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_addition.dart new file mode 100644 index 000000000000..7a8ef1dbd4f8 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_addition.dart @@ -0,0 +1,11 @@ +// 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. + +// ignore: avoid_classes_with_only_static_members +/// The interface that platform implementations must implement when they want to +/// provide platform specific in_app_purchase features. +abstract class InAppPurchaseAddition { + /// The instance containing the platform specific in_app_purchase features. + static InAppPurchaseAddition? instance; +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_addition_provider.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_addition_provider.dart new file mode 100644 index 000000000000..97c8051a3e3e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_addition_provider.dart @@ -0,0 +1,17 @@ +// 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 'package:in_app_purchase_platform_interface/src/in_app_purchase_addition.dart'; + +/// The [InAppPurchaseAdditionProvider] is responsible for providing +/// a platform specific [InAppPurchaseAddition]. +/// +/// [InAppPurchaseAddition] implementation contain platform specific +/// features that are not available from the platform idiomatic +/// [InAppPurchasePlatform] API. +abstract class InAppPurchaseAdditionProvider { + /// Provides a platform specific implementation of the [InAppPurchaseAddition] + /// class. + T getPlatformAddition(); +} From 9eb358fee78102af78685b38da1a486861b1332c Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 22 Apr 2021 10:14:51 +0200 Subject: [PATCH 16/17] Document the addition functionality in README --- .../in_app_purchase_platform_interface/README.md | 9 ++++++++- .../lib/in_app_purchase_platform_interface.dart | 4 ++-- ...dart => in_app_purchase_platform_addition.dart} | 4 ++-- ...n_app_purchase_platform_addition_provider.dart} | 14 +++++++------- 4 files changed, 19 insertions(+), 12 deletions(-) rename packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/{in_app_purchase_addition.dart => in_app_purchase_platform_addition.dart} (81%) rename packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/{in_app_purchase_addition_provider.dart => in_app_purchase_platform_addition_provider.dart} (51%) diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/README.md b/packages/in_app_purchase/in_app_purchase_platform_interface/README.md index 9dc71b566f1f..158af910b63d 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/README.md +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/README.md @@ -14,6 +14,12 @@ platform-specific behavior, and when you register your plugin, set the default `InAppPurchasePlatform` by calling `InAppPurchasePlatform.setInstance(MyPlatformInAppPurchase())`. +To implement functionality that is specific to the platform and is not covered +by the [`InAppPurchasePlatform`][2] idiomatic API, extend +[`InAppPurchasePlatformAddition`][3] with the platform-specific functionality, +and when the plugin is registered, set the addition instance by calling the +`InAppPurchasePlatformAddition.instance = MyPlatformInAppPurchaseAddition()`. + # Note on breaking changes Strongly prefer non-breaking changes (such as adding a method to the interface) @@ -23,4 +29,5 @@ 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 \ No newline at end of file +[2]: lib/in_app_purchase_platform_interface.dart +[3]: lib/in_app_purchase_platform_addition.dart \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart index 879bca786781..9e12a9dd33e4 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart @@ -2,7 +2,7 @@ // 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_addition.dart'; -export 'src/in_app_purchase_addition_provider.dart'; export 'src/in_app_purchase_platform.dart'; +export 'src/in_app_purchase_platform_addition.dart'; +export 'src/in_app_purchase_platform_addition_provider.dart'; export 'src/types/types.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_addition.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition.dart similarity index 81% rename from packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_addition.dart rename to packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition.dart index 7a8ef1dbd4f8..832b786af308 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_addition.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition.dart @@ -5,7 +5,7 @@ // ignore: avoid_classes_with_only_static_members /// The interface that platform implementations must implement when they want to /// provide platform specific in_app_purchase features. -abstract class InAppPurchaseAddition { +abstract class InAppPurchasePlatformAddition { /// The instance containing the platform specific in_app_purchase features. - static InAppPurchaseAddition? instance; + static InAppPurchasePlatformAddition? instance; } diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_addition_provider.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition_provider.dart similarity index 51% rename from packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_addition_provider.dart rename to packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition_provider.dart index 97c8051a3e3e..d981f73b4019 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_addition_provider.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition_provider.dart @@ -2,16 +2,16 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:in_app_purchase_platform_interface/src/in_app_purchase_addition.dart'; +import 'package:in_app_purchase_platform_interface/src/in_app_purchase_platform_addition.dart'; -/// The [InAppPurchaseAdditionProvider] is responsible for providing -/// a platform specific [InAppPurchaseAddition]. +/// The [InAppPurchasePlatformAdditionProvider] is responsible for providing +/// a platform specific [InAppPurchasePlatformAddition]. /// -/// [InAppPurchaseAddition] implementation contain platform specific +/// [InAppPurchasePlatformAddition] implementation contain platform specific /// features that are not available from the platform idiomatic /// [InAppPurchasePlatform] API. -abstract class InAppPurchaseAdditionProvider { - /// Provides a platform specific implementation of the [InAppPurchaseAddition] +abstract class InAppPurchasePlatformAdditionProvider { + /// Provides a platform specific implementation of the [InAppPurchasePlatformAddition] /// class. - T getPlatformAddition(); + T getPlatformAddition(); } From 33d1118b841a4939d6b8f4b517178dabbbec4a1b Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 22 Apr 2021 11:19:39 +0200 Subject: [PATCH 17/17] Added example code and documentation --- .../README.md | 2 +- .../in_app_purchase_platform_addition.dart | 31 ++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/README.md b/packages/in_app_purchase/in_app_purchase_platform_interface/README.md index 158af910b63d..91585dbfc88f 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/README.md +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/README.md @@ -17,7 +17,7 @@ platform-specific behavior, and when you register your plugin, set the default To implement functionality that is specific to the platform and is not covered by the [`InAppPurchasePlatform`][2] idiomatic API, extend [`InAppPurchasePlatformAddition`][3] with the platform-specific functionality, -and when the plugin is registered, set the addition instance by calling the +and when the plugin is registered, set the addition instance by calling `InAppPurchasePlatformAddition.instance = MyPlatformInAppPurchaseAddition()`. # Note on breaking changes diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition.dart index 832b786af308..5c41f138ecea 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition.dart @@ -6,6 +6,35 @@ /// The interface that platform implementations must implement when they want to /// provide platform specific in_app_purchase features. abstract class InAppPurchasePlatformAddition { - /// The instance containing the platform specific in_app_purchase features. + /// The instance containing the platform-specific in_app_purchase + /// functionality. + /// + /// To implement additional functionality extend + /// [`InAppPurchasePlatformAddition`][3] with the platform-specific + /// functionality, and when the plugin is registered, set the + /// `InAppPurchasePlatformAddition.instance` with the new addition + /// implementationinstance. + /// + /// Example implementation might look like this: + /// ```dart + /// class InAppPurchaseMyPlatformAddition extends InAppPurchasePlatformAddition { + /// Future myPlatformMethod() {} + /// } + /// ``` + /// + /// The following snippit shows how to register the `InAppPurchaseMyPlatformAddition`: + /// ```dart + /// class InAppPurchaseMyPlatformPlugin { + /// static void registerWith(Registrar registrar) { + /// // Register the platform-specific implementation of the idiomatic + /// // InAppPurchase API. + /// InAppPurchasePlatform.instance = InAppPurchaseMyPlatformPlugin(); + /// + /// // Register the [InAppPurchaseMyPlatformAddition] containing the + /// // platform-specific functionality. + /// InAppPurchasePlatformAddition.instance = InAppPurchaseMyPlatformAddition(); + /// } + /// } + /// ``` static InAppPurchasePlatformAddition? instance; }