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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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; } From a7cea45b69d3faadd101b032180678408d3a3d3f Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 22 Apr 2021 13:23:30 +0200 Subject: [PATCH 18/33] Reference the platform_interface --- packages/in_app_purchase/in_app_purchase/pubspec.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/in_app_purchase/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/in_app_purchase/pubspec.yaml index c7226078c722..75962361ed4f 100644 --- a/packages/in_app_purchase/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase/pubspec.yaml @@ -3,6 +3,9 @@ description: A Flutter plugin for in-app purchases. Exposes APIs for making in-a homepage: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase version: 0.5.2 +# TODO(mvanbeusekom): Remove when in_app_purchase_platform_interface is published +publish_to: 'none' + dependencies: flutter: sdk: flutter @@ -10,6 +13,10 @@ dependencies: meta: ^1.3.0 collection: ^1.15.0 + # TODO(mvanbeusekom): Replace with pub.dev version when in_app_purchase_platform_interface is published + in_app_purchase_platform_interface: + path: ../in_app_purchase_platform_interface + dev_dependencies: build_runner: ^1.11.1 json_serializable: ^4.0.0 From 06dd7554267722cc7d7acf6727c89019e9c39fd2 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 26 Apr 2021 18:13:05 +0200 Subject: [PATCH 19/33] Initial iOS specific implementation --- .../in_app_purchase_ios/CHANGELOG.md | 3 + .../in_app_purchase_ios/LICENSE | 25 ++ .../in_app_purchase_ios/analysis_options.yaml | 1 + .../in_app_purchase_ios/build.yaml | 7 + .../in_app_purchase_ios/ios/Assets/.gitkeep | 0 .../ios/Classes/FIAObjectTranslator.h | 35 ++ .../ios/Classes/FIAObjectTranslator.m | 172 ++++++++ .../ios/Classes/FIAPReceiptManager.h | 17 + .../ios/Classes/FIAPReceiptManager.m | 26 ++ .../ios/Classes/FIAPRequestHandler.h | 20 + .../ios/Classes/FIAPRequestHandler.m | 55 +++ .../ios/Classes/FIAPaymentQueueHandler.h | 46 +++ .../ios/Classes/FIAPaymentQueueHandler.m | 122 ++++++ .../ios/Classes/InAppPurchasePlugin.h | 17 + .../ios/Classes/InAppPurchasePlugin.m | 360 ++++++++++++++++ .../ios/Tests/InAppPurchasePluginTest.m | 304 ++++++++++++++ .../ios/Tests/PaymentQueueTest.m | 212 ++++++++++ .../ios/Tests/ProductRequestHandlerTest.m | 89 ++++ .../in_app_purchase_ios/ios/Tests/Stubs.h | 62 +++ .../in_app_purchase_ios/ios/Tests/Stubs.m | 290 +++++++++++++ .../ios/Tests/TranslatorTest.m | 147 +++++++ .../ios/in_app_purchase.podspec | 27 ++ .../lib/in_app_purchase_ios.dart | 6 + .../in_app_purchase_ios/lib/src/channel.dart | 9 + .../lib/src/in_app_purchase_ios_platform.dart | 236 +++++++++++ .../src/models/app_store_product_details.dart | 47 +++ .../models/app_store_purchase_details.dart | 66 +++ .../src/models/app_store_purchase_param.dart | 26 ++ .../lib/src/models/models.dart | 7 + .../lib/src/store_kit_wrappers/README.md | 5 + .../store_kit_wrappers/enum_converters.dart | 108 +++++ .../store_kit_wrappers/enum_converters.g.dart | 73 ++++ .../sk_payment_queue_wrapper.dart | 384 ++++++++++++++++++ .../sk_payment_queue_wrapper.g.dart | 44 ++ .../sk_payment_transaction_wrappers.dart | 203 +++++++++ .../sk_payment_transaction_wrappers.g.dart | 37 ++ .../sk_product_wrapper.dart | 374 +++++++++++++++++ .../sk_product_wrapper.g.dart | 123 ++++++ .../sk_receipt_manager.dart | 23 ++ .../store_kit_wrappers/sk_request_maker.dart | 59 +++ .../lib/store_kit_wrappers.dart | 9 + .../in_app_purchase_ios/pubspec.yaml | 33 ++ .../lib/src/in_app_purchase_platform.dart | 8 +- .../lib/src/types/product_details.dart | 13 + 44 files changed, 3928 insertions(+), 2 deletions(-) create mode 100644 packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md create mode 100644 packages/in_app_purchase/in_app_purchase_ios/LICENSE create mode 100644 packages/in_app_purchase/in_app_purchase_ios/analysis_options.yaml create mode 100644 packages/in_app_purchase/in_app_purchase_ios/build.yaml create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/Assets/.gitkeep create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.h create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPRequestHandler.h create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPRequestHandler.m create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.h create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/Tests/InAppPurchasePluginTest.m create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/Tests/PaymentQueueTest.m create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/Tests/ProductRequestHandlerTest.m create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/Tests/Stubs.h create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/Tests/Stubs.m create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/Tests/TranslatorTest.m create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase.podspec create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/in_app_purchase_ios.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/channel.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/models/app_store_product_details.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/models/app_store_purchase_details.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/models/app_store_purchase_param.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/models/models.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/README.md create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.g.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_receipt_manager.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_request_maker.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/store_kit_wrappers.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml diff --git a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md new file mode 100644 index 000000000000..2f529b31655d --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/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_ios/LICENSE b/packages/in_app_purchase/in_app_purchase_ios/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/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_ios/analysis_options.yaml b/packages/in_app_purchase/in_app_purchase_ios/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml diff --git a/packages/in_app_purchase/in_app_purchase_ios/build.yaml b/packages/in_app_purchase/in_app_purchase_ios/build.yaml new file mode 100644 index 000000000000..e15cf14b85fd --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/build.yaml @@ -0,0 +1,7 @@ +targets: + $default: + builders: + json_serializable: + options: + any_map: true + create_to_json: true diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Assets/.gitkeep b/packages/in_app_purchase/in_app_purchase_ios/ios/Assets/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h new file mode 100644 index 000000000000..2d0187e88aed --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h @@ -0,0 +1,35 @@ +// 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 +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FIAObjectTranslator : NSObject + ++ (NSDictionary *)getMapFromSKProduct:(SKProduct *)product; + ++ (NSDictionary *)getMapFromSKProductSubscriptionPeriod:(SKProductSubscriptionPeriod *)period + API_AVAILABLE(ios(11.2)); + ++ (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount + API_AVAILABLE(ios(11.2)); + ++ (NSDictionary *)getMapFromSKProductsResponse:(SKProductsResponse *)productResponse; + ++ (NSDictionary *)getMapFromSKPayment:(SKPayment *)payment; + ++ (NSDictionary *)getMapFromNSLocale:(NSLocale *)locale; + ++ (SKMutablePayment *)getSKMutablePaymentFromMap:(NSDictionary *)map; + ++ (NSDictionary *)getMapFromSKPaymentTransaction:(SKPaymentTransaction *)transaction; + ++ (NSDictionary *)getMapFromNSError:(NSError *)error; + +@end +; + +NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m new file mode 100644 index 000000000000..5d6e0a244a96 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m @@ -0,0 +1,172 @@ +// 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 "FIAObjectTranslator.h" + +#pragma mark - SKProduct Coders + +@implementation FIAObjectTranslator + ++ (NSDictionary *)getMapFromSKProduct:(SKProduct *)product { + if (!product) { + return nil; + } + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"localizedDescription" : product.localizedDescription ?: [NSNull null], + @"localizedTitle" : product.localizedTitle ?: [NSNull null], + @"productIdentifier" : product.productIdentifier ?: [NSNull null], + @"price" : product.price.description ?: [NSNull null] + + }]; + // TODO(cyanglaz): NSLocale is a complex object, want to see the actual need of getting this + // expanded to a map. Matching android to only get the currencySymbol for now. + // https://github.com/flutter/flutter/issues/26610 + [map setObject:[FIAObjectTranslator getMapFromNSLocale:product.priceLocale] ?: [NSNull null] + forKey:@"priceLocale"]; + if (@available(iOS 11.2, *)) { + [map setObject:[FIAObjectTranslator + getMapFromSKProductSubscriptionPeriod:product.subscriptionPeriod] + ?: [NSNull null] + forKey:@"subscriptionPeriod"]; + } + if (@available(iOS 11.2, *)) { + [map setObject:[FIAObjectTranslator getMapFromSKProductDiscount:product.introductoryPrice] + ?: [NSNull null] + forKey:@"introductoryPrice"]; + } + if (@available(iOS 12.0, *)) { + [map setObject:product.subscriptionGroupIdentifier ?: [NSNull null] + forKey:@"subscriptionGroupIdentifier"]; + } + return map; +} + ++ (NSDictionary *)getMapFromSKProductSubscriptionPeriod:(SKProductSubscriptionPeriod *)period { + if (!period) { + return nil; + } + return @{@"numberOfUnits" : @(period.numberOfUnits), @"unit" : @(period.unit)}; +} + ++ (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount { + if (!discount) { + return nil; + } + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"price" : discount.price.description ?: [NSNull null], + @"numberOfPeriods" : @(discount.numberOfPeriods), + @"subscriptionPeriod" : + [FIAObjectTranslator getMapFromSKProductSubscriptionPeriod:discount.subscriptionPeriod] + ?: [NSNull null], + @"paymentMode" : @(discount.paymentMode) + }]; + + // TODO(cyanglaz): NSLocale is a complex object, want to see the actual need of getting this + // expanded to a map. Matching android to only get the currencySymbol for now. + // https://github.com/flutter/flutter/issues/26610 + [map setObject:[FIAObjectTranslator getMapFromNSLocale:discount.priceLocale] ?: [NSNull null] + forKey:@"priceLocale"]; + return map; +} + ++ (NSDictionary *)getMapFromSKProductsResponse:(SKProductsResponse *)productResponse { + if (!productResponse) { + return nil; + } + NSMutableArray *productsMapArray = [NSMutableArray new]; + for (SKProduct *product in productResponse.products) { + [productsMapArray addObject:[FIAObjectTranslator getMapFromSKProduct:product]]; + } + return @{ + @"products" : productsMapArray, + @"invalidProductIdentifiers" : productResponse.invalidProductIdentifiers ?: @[] + }; +} + ++ (NSDictionary *)getMapFromSKPayment:(SKPayment *)payment { + if (!payment) { + return nil; + } + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"productIdentifier" : payment.productIdentifier ?: [NSNull null], + @"requestData" : payment.requestData ? [[NSString alloc] initWithData:payment.requestData + encoding:NSUTF8StringEncoding] + : [NSNull null], + @"quantity" : @(payment.quantity), + @"applicationUsername" : payment.applicationUsername ?: [NSNull null] + }]; + if (@available(iOS 8.3, *)) { + [map setObject:@(payment.simulatesAskToBuyInSandbox) forKey:@"simulatesAskToBuyInSandbox"]; + } + return map; +} + ++ (NSDictionary *)getMapFromNSLocale:(NSLocale *)locale { + if (!locale) { + return nil; + } + NSMutableDictionary *map = [[NSMutableDictionary alloc] init]; + [map setObject:[locale objectForKey:NSLocaleCurrencySymbol] ?: [NSNull null] + forKey:@"currencySymbol"]; + [map setObject:[locale objectForKey:NSLocaleCurrencyCode] ?: [NSNull null] + forKey:@"currencyCode"]; + return map; +} + ++ (SKMutablePayment *)getSKMutablePaymentFromMap:(NSDictionary *)map { + if (!map) { + return nil; + } + SKMutablePayment *payment = [[SKMutablePayment alloc] init]; + payment.productIdentifier = map[@"productIdentifier"]; + NSString *utf8String = map[@"requestData"]; + payment.requestData = [utf8String dataUsingEncoding:NSUTF8StringEncoding]; + payment.quantity = [map[@"quantity"] integerValue]; + payment.applicationUsername = map[@"applicationUsername"]; + if (@available(iOS 8.3, *)) { + payment.simulatesAskToBuyInSandbox = [map[@"simulatesAskToBuyInSandbox"] boolValue]; + } + return payment; +} + ++ (NSDictionary *)getMapFromSKPaymentTransaction:(SKPaymentTransaction *)transaction { + if (!transaction) { + return nil; + } + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"error" : [FIAObjectTranslator getMapFromNSError:transaction.error] ?: [NSNull null], + @"payment" : transaction.payment ? [FIAObjectTranslator getMapFromSKPayment:transaction.payment] + : [NSNull null], + @"originalTransaction" : transaction.originalTransaction + ? [FIAObjectTranslator getMapFromSKPaymentTransaction:transaction.originalTransaction] + : [NSNull null], + @"transactionTimeStamp" : transaction.transactionDate + ? @(transaction.transactionDate.timeIntervalSince1970) + : [NSNull null], + @"transactionIdentifier" : transaction.transactionIdentifier ?: [NSNull null], + @"transactionState" : @(transaction.transactionState) + }]; + + return map; +} + ++ (NSDictionary *)getMapFromNSError:(NSError *)error { + if (!error) { + return nil; + } + NSMutableDictionary *userInfo = [NSMutableDictionary new]; + for (NSErrorUserInfoKey key in error.userInfo) { + id value = error.userInfo[key]; + if ([value isKindOfClass:[NSError class]]) { + userInfo[key] = [FIAObjectTranslator getMapFromNSError:value]; + } else if ([value isKindOfClass:[NSURL class]]) { + userInfo[key] = [value absoluteString]; + } else { + userInfo[key] = value; + } + } + return @{@"code" : @(error.code), @"domain" : error.domain ?: @"", @"userInfo" : userInfo}; +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.h new file mode 100644 index 000000000000..94020ff2348b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.h @@ -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 + +NS_ASSUME_NONNULL_BEGIN + +@class FlutterError; + +@interface FIAPReceiptManager : NSObject + +- (nullable NSString *)retrieveReceiptWithError:(FlutterError *_Nullable *_Nullable)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m new file mode 100644 index 000000000000..526364020ad3 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m @@ -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 "FIAPReceiptManager.h" +#import + +@implementation FIAPReceiptManager + +- (NSString *)retrieveReceiptWithError:(FlutterError **)error { + NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; + NSData *receipt = [self getReceiptData:receiptURL]; + if (!receipt) { + *error = [FlutterError errorWithCode:@"storekit_no_receipt" + message:@"Cannot find receipt for the current main bundle." + details:nil]; + return nil; + } + return [receipt base64EncodedStringWithOptions:kNilOptions]; +} + +- (NSData *)getReceiptData:(NSURL *)url { + return [NSData dataWithContentsOfURL:url]; +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPRequestHandler.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPRequestHandler.h new file mode 100644 index 000000000000..cbf21d6e161f --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPRequestHandler.h @@ -0,0 +1,20 @@ +// 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 +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^ProductRequestCompletion)(SKProductsResponse *_Nullable response, + NSError *_Nullable errror); + +@interface FIAPRequestHandler : NSObject + +- (instancetype)initWithRequest:(SKRequest *)request; +- (void)startProductRequestWithCompletionHandler:(ProductRequestCompletion)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPRequestHandler.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPRequestHandler.m new file mode 100644 index 000000000000..8767265d8544 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPRequestHandler.m @@ -0,0 +1,55 @@ +// 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 "FIAPRequestHandler.h" +#import + +#pragma mark - Main Handler + +@interface FIAPRequestHandler () + +@property(copy, nonatomic) ProductRequestCompletion completion; +@property(strong, nonatomic) SKRequest *request; + +@end + +@implementation FIAPRequestHandler + +- (instancetype)initWithRequest:(SKRequest *)request { + self = [super init]; + if (self) { + self.request = request; + request.delegate = self; + } + return self; +} + +- (void)startProductRequestWithCompletionHandler:(ProductRequestCompletion)completion { + self.completion = completion; + [self.request start]; +} + +- (void)productsRequest:(SKProductsRequest *)request + didReceiveResponse:(SKProductsResponse *)response { + if (self.completion) { + self.completion(response, nil); + // set the completion to nil here so self.completion won't be triggered again in + // requestDidFinish for SKProductRequest. + self.completion = nil; + } +} + +- (void)requestDidFinish:(SKRequest *)request { + if (self.completion) { + self.completion(nil, nil); + } +} + +- (void)request:(SKRequest *)request didFailWithError:(NSError *)error { + if (self.completion) { + self.completion(nil, error); + } +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h new file mode 100644 index 000000000000..fddeb07e01a3 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h @@ -0,0 +1,46 @@ +// 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 +#import + +@class SKPaymentTransaction; + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^TransactionsUpdated)(NSArray *transactions); +typedef void (^TransactionsRemoved)(NSArray *transactions); +typedef void (^RestoreTransactionFailed)(NSError *error); +typedef void (^RestoreCompletedTransactionsFinished)(void); +typedef BOOL (^ShouldAddStorePayment)(SKPayment *payment, SKProduct *product); +typedef void (^UpdatedDownloads)(NSArray *downloads); + +@interface FIAPaymentQueueHandler : NSObject + +- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue + transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated + transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved + restoreTransactionFailed:(nullable RestoreTransactionFailed)restoreTransactionFailed + restoreCompletedTransactionsFinished: + (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished + shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment + updatedDownloads:(nullable UpdatedDownloads)updatedDownloads; +// Can throw exceptions if the transaction type is purchasing, should always used in a @try block. +- (void)finishTransaction:(nonnull SKPaymentTransaction *)transaction; +- (void)restoreTransactions:(nullable NSString *)applicationName; +- (void)presentCodeRedemptionSheet; +- (NSArray *)getUnfinishedTransactions; + +// This method needs to be called before any other methods. +- (void)startObservingPaymentQueue; + +// Appends a payment to the SKPaymentQueue. +// +// @param payment Payment object to be added to the payment queue. +// @return whether "addPayment" was successful. +- (BOOL)addPayment:(SKPayment *)payment; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m new file mode 100644 index 000000000000..eb3348e4b3c9 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m @@ -0,0 +1,122 @@ +// 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 "FIAPaymentQueueHandler.h" + +@interface FIAPaymentQueueHandler () + +@property(strong, nonatomic) SKPaymentQueue *queue; +@property(nullable, copy, nonatomic) TransactionsUpdated transactionsUpdated; +@property(nullable, copy, nonatomic) TransactionsRemoved transactionsRemoved; +@property(nullable, copy, nonatomic) RestoreTransactionFailed restoreTransactionFailed; +@property(nullable, copy, nonatomic) + RestoreCompletedTransactionsFinished paymentQueueRestoreCompletedTransactionsFinished; +@property(nullable, copy, nonatomic) ShouldAddStorePayment shouldAddStorePayment; +@property(nullable, copy, nonatomic) UpdatedDownloads updatedDownloads; + +@end + +@implementation FIAPaymentQueueHandler + +- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue + transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated + transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved + restoreTransactionFailed:(nullable RestoreTransactionFailed)restoreTransactionFailed + restoreCompletedTransactionsFinished: + (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished + shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment + updatedDownloads:(nullable UpdatedDownloads)updatedDownloads { + self = [super init]; + if (self) { + _queue = queue; + _transactionsUpdated = transactionsUpdated; + _transactionsRemoved = transactionsRemoved; + _restoreTransactionFailed = restoreTransactionFailed; + _paymentQueueRestoreCompletedTransactionsFinished = restoreCompletedTransactionsFinished; + _shouldAddStorePayment = shouldAddStorePayment; + _updatedDownloads = updatedDownloads; + } + return self; +} + +- (void)startObservingPaymentQueue { + [_queue addTransactionObserver:self]; +} + +- (BOOL)addPayment:(SKPayment *)payment { + for (SKPaymentTransaction *transaction in self.queue.transactions) { + if ([transaction.payment.productIdentifier isEqualToString:payment.productIdentifier]) { + return NO; + } + } + [self.queue addPayment:payment]; + return YES; +} + +- (void)finishTransaction:(SKPaymentTransaction *)transaction { + [self.queue finishTransaction:transaction]; +} + +- (void)restoreTransactions:(nullable NSString *)applicationName { + if (applicationName) { + [self.queue restoreCompletedTransactionsWithApplicationUsername:applicationName]; + } else { + [self.queue restoreCompletedTransactions]; + } +} + +- (void)presentCodeRedemptionSheet { + if (@available(iOS 14, *)) { + [self.queue presentCodeRedemptionSheet]; + } else { + NSLog(@"presentCodeRedemptionSheet is only available on iOS 14 or newer"); + } +} + +#pragma mark - observing + +// Sent when the transaction array has changed (additions or state changes). Client should check +// state of transactions and finish as appropriate. +- (void)paymentQueue:(SKPaymentQueue *)queue + updatedTransactions:(NSArray *)transactions { + // notify dart through callbacks. + self.transactionsUpdated(transactions); +} + +// Sent when transactions are removed from the queue (via finishTransaction:). +- (void)paymentQueue:(SKPaymentQueue *)queue + removedTransactions:(NSArray *)transactions { + self.transactionsRemoved(transactions); +} + +// Sent when an error is encountered while adding transactions from the user's purchase history back +// to the queue. +- (void)paymentQueue:(SKPaymentQueue *)queue + restoreCompletedTransactionsFailedWithError:(NSError *)error { + self.restoreTransactionFailed(error); +} + +// Sent when all transactions from the user's purchase history have successfully been added back to +// the queue. +- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue { + self.paymentQueueRestoreCompletedTransactionsFinished(); +} + +// Sent when the download state has changed. +- (void)paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray *)downloads { + self.updatedDownloads(downloads); +} + +// Sent when a user initiates an IAP buy from the App Store +- (BOOL)paymentQueue:(SKPaymentQueue *)queue + shouldAddStorePayment:(SKPayment *)payment + forProduct:(SKProduct *)product { + return (self.shouldAddStorePayment(payment, product)); +} + +- (NSArray *)getUnfinishedTransactions { + return self.queue.transactions; +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.h new file mode 100644 index 000000000000..8cb42f3fe8c2 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.h @@ -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 +@class FIAPaymentQueueHandler; +@class FIAPReceiptManager; + +@interface InAppPurchasePlugin : NSObject + +@property(strong, nonatomic) FIAPaymentQueueHandler *paymentQueueHandler; + +- (instancetype)initWithReceiptManager:(FIAPReceiptManager *)receiptManager + NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m new file mode 100644 index 000000000000..650cd812d470 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m @@ -0,0 +1,360 @@ +// 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 "InAppPurchasePlugin.h" +#import +#import "FIAObjectTranslator.h" +#import "FIAPReceiptManager.h" +#import "FIAPRequestHandler.h" +#import "FIAPaymentQueueHandler.h" + +@interface InAppPurchasePlugin () + +// Holding strong references to FIAPRequestHandlers. Remove the handlers from the set after +// the request is finished. +@property(strong, nonatomic, readonly) NSMutableSet *requestHandlers; + +// After querying the product, the available products will be saved in the map to be used +// for purchase. +@property(strong, nonatomic, readonly) NSMutableDictionary *productsCache; + +// Call back channel to dart used for when a listener function is triggered. +@property(strong, nonatomic, readonly) FlutterMethodChannel *callbackChannel; +@property(strong, nonatomic, readonly) NSObject *registry; +@property(strong, nonatomic, readonly) NSObject *messenger; +@property(strong, nonatomic, readonly) NSObject *registrar; + +@property(strong, nonatomic, readonly) FIAPReceiptManager *receiptManager; + +@end + +@implementation InAppPurchasePlugin + ++ (void)registerWithRegistrar:(NSObject *)registrar { + FlutterMethodChannel *channel = + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" + binaryMessenger:[registrar messenger]]; + InAppPurchasePlugin *instance = [[InAppPurchasePlugin alloc] initWithRegistrar:registrar]; + [registrar addMethodCallDelegate:instance channel:channel]; +} + +- (instancetype)initWithReceiptManager:(FIAPReceiptManager *)receiptManager { + self = [super init]; + _receiptManager = receiptManager; + _requestHandlers = [NSMutableSet new]; + _productsCache = [NSMutableDictionary new]; + return self; +} + +- (instancetype)initWithRegistrar:(NSObject *)registrar { + self = [self initWithReceiptManager:[FIAPReceiptManager new]]; + _registrar = registrar; + _registry = [registrar textures]; + _messenger = [registrar messenger]; + + __weak typeof(self) weakSelf = self; + _paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueue defaultQueue] + transactionsUpdated:^(NSArray *_Nonnull transactions) { + [weakSelf handleTransactionsUpdated:transactions]; + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + [weakSelf handleTransactionsRemoved:transactions]; + } + restoreTransactionFailed:^(NSError *_Nonnull error) { + [weakSelf handleTransactionRestoreFailed:error]; + } + restoreCompletedTransactionsFinished:^{ + [weakSelf restoreCompletedTransactionsFinished]; + } + shouldAddStorePayment:^BOOL(SKPayment *payment, SKProduct *product) { + return [weakSelf shouldAddStorePayment:payment product:product]; + } + updatedDownloads:^void(NSArray *_Nonnull downloads) { + [weakSelf updatedDownloads:downloads]; + }]; + [_paymentQueueHandler startObservingPaymentQueue]; + _callbackChannel = + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" + binaryMessenger:[registrar messenger]]; + return self; +} + +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + if ([@"-[SKPaymentQueue canMakePayments:]" isEqualToString:call.method]) { + [self canMakePayments:result]; + } else if ([@"-[SKPaymentQueue transactions]" isEqualToString:call.method]) { + [self getPendingTransactions:result]; + } else if ([@"-[InAppPurchasePlugin startProductRequest:result:]" isEqualToString:call.method]) { + [self handleProductRequestMethodCall:call result:result]; + } else if ([@"-[InAppPurchasePlugin addPayment:result:]" isEqualToString:call.method]) { + [self addPayment:call result:result]; + } else if ([@"-[InAppPurchasePlugin finishTransaction:result:]" isEqualToString:call.method]) { + [self finishTransaction:call result:result]; + } else if ([@"-[InAppPurchasePlugin restoreTransactions:result:]" isEqualToString:call.method]) { + [self restoreTransactions:call result:result]; + } else if ([@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]" + isEqualToString:call.method]) { + [self presentCodeRedemptionSheet:call result:result]; + } else if ([@"-[InAppPurchasePlugin retrieveReceiptData:result:]" isEqualToString:call.method]) { + [self retrieveReceiptData:call result:result]; + } else if ([@"-[InAppPurchasePlugin refreshReceipt:result:]" isEqualToString:call.method]) { + [self refreshReceipt:call result:result]; + } else { + result(FlutterMethodNotImplemented); + } +} + +- (void)canMakePayments:(FlutterResult)result { + result([NSNumber numberWithBool:[SKPaymentQueue canMakePayments]]); +} + +- (void)getPendingTransactions:(FlutterResult)result { + NSArray *transactions = + [self.paymentQueueHandler getUnfinishedTransactions]; + NSMutableArray *transactionMaps = [[NSMutableArray alloc] init]; + for (SKPaymentTransaction *transaction in transactions) { + [transactionMaps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; + } + result(transactionMaps); +} + +- (void)handleProductRequestMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + if (![call.arguments isKindOfClass:[NSArray class]]) { + result([FlutterError errorWithCode:@"storekit_invalid_argument" + message:@"Argument type of startRequest is not array" + details:call.arguments]); + return; + } + NSArray *productIdentifiers = (NSArray *)call.arguments; + SKProductsRequest *request = + [self getProductRequestWithIdentifiers:[NSSet setWithArray:productIdentifiers]]; + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + [self.requestHandlers addObject:handler]; + __weak typeof(self) weakSelf = self; + [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + result([FlutterError errorWithCode:@"storekit_getproductrequest_platform_error" + message:error.localizedDescription + details:error.description]); + return; + } + if (!response) { + result([FlutterError errorWithCode:@"storekit_platform_no_response" + message:@"Failed to get SKProductResponse in startRequest " + @"call. Error occured on iOS platform" + details:call.arguments]); + return; + } + for (SKProduct *product in response.products) { + [self.productsCache setObject:product forKey:product.productIdentifier]; + } + result([FIAObjectTranslator getMapFromSKProductsResponse:response]); + [weakSelf.requestHandlers removeObject:handler]; + }]; +} + +- (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { + if (![call.arguments isKindOfClass:[NSDictionary class]]) { + result([FlutterError errorWithCode:@"storekit_invalid_argument" + message:@"Argument type of addPayment is not a Dictionary" + details:call.arguments]); + return; + } + NSDictionary *paymentMap = (NSDictionary *)call.arguments; + NSString *productID = [paymentMap objectForKey:@"productIdentifier"]; + // When a product is already fetched, we create a payment object with + // the product to process the payment. + SKProduct *product = [self getProduct:productID]; + if (!product) { + result([FlutterError + errorWithCode:@"storekit_invalid_payment_object" + message: + @"You have requested a payment for an invalid product. Either the " + @"`productIdentifier` of the payment is not valid or the product has not been " + @"fetched before adding the payment to the payment queue." + details:call.arguments]); + return; + } + SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product]; + payment.applicationUsername = [paymentMap objectForKey:@"applicationUsername"]; + NSNumber *quantity = [paymentMap objectForKey:@"quantity"]; + payment.quantity = (quantity != nil) ? quantity.integerValue : 1; + if (@available(iOS 8.3, *)) { + NSNumber *simulatesAskToBuyInSandbox = [paymentMap objectForKey:@"simulatesAskToBuyInSandbox"]; + payment.simulatesAskToBuyInSandbox = (id)simulatesAskToBuyInSandbox == (id)[NSNull null] + ? NO + : [simulatesAskToBuyInSandbox boolValue]; + } + + if (![self.paymentQueueHandler addPayment:payment]) { + result([FlutterError + errorWithCode:@"storekit_duplicate_product_object" + message:@"There is a pending transaction for the same product identifier. Please " + @"either wait for it to be finished or finish it manually using " + @"`completePurchase` to avoid edge cases." + + details:call.arguments]); + return; + } + result(nil); +} + +- (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result { + if (![call.arguments isKindOfClass:[NSDictionary class]]) { + result([FlutterError errorWithCode:@"storekit_invalid_argument" + message:@"Argument type of finishTransaction is not a Dictionary" + details:call.arguments]); + return; + } + NSDictionary *paymentMap = (NSDictionary *)call.arguments; + NSString *transactionIdentifier = [paymentMap objectForKey:@"transactionIdentifier"]; + NSString *productIdentifier = [paymentMap objectForKey:@"productIdentifier"]; + + NSArray *pendingTransactions = + [self.paymentQueueHandler getUnfinishedTransactions]; + + for (SKPaymentTransaction *transaction in pendingTransactions) { + // If the user cancels the purchase dialog we won't have a transactionIdentifier. + // So if it is null AND a transaction in the pendingTransactions list has + // also a null transactionIdentifier we check for equal product identifiers. + if ([transaction.transactionIdentifier isEqualToString:transactionIdentifier] || + ([transactionIdentifier isEqual:[NSNull null]] && + transaction.transactionIdentifier == nil && + [transaction.payment.productIdentifier isEqualToString:productIdentifier])) { + @try { + [self.paymentQueueHandler finishTransaction:transaction]; + } @catch (NSException *e) { + result([FlutterError errorWithCode:@"storekit_finish_transaction_exception" + message:e.name + details:e.description]); + return; + } + } + } + + result(nil); +} + +- (void)restoreTransactions:(FlutterMethodCall *)call result:(FlutterResult)result { + if (call.arguments && ![call.arguments isKindOfClass:[NSString class]]) { + result([FlutterError + errorWithCode:@"storekit_invalid_argument" + message:@"Argument is not nil and the type of finishTransaction is not a string." + details:call.arguments]); + return; + } + [self.paymentQueueHandler restoreTransactions:call.arguments]; + result(nil); +} + +- (void)presentCodeRedemptionSheet:(FlutterMethodCall *)call result:(FlutterResult)result { + [self.paymentQueueHandler presentCodeRedemptionSheet]; + result(nil); +} + +- (void)retrieveReceiptData:(FlutterMethodCall *)call result:(FlutterResult)result { + FlutterError *error = nil; + NSString *receiptData = [self.receiptManager retrieveReceiptWithError:&error]; + if (error) { + result(error); + return; + } + result(receiptData); +} + +- (void)refreshReceipt:(FlutterMethodCall *)call result:(FlutterResult)result { + NSDictionary *arguments = call.arguments; + SKReceiptRefreshRequest *request; + if (arguments) { + if (![arguments isKindOfClass:[NSDictionary class]]) { + result([FlutterError errorWithCode:@"storekit_invalid_argument" + message:@"Argument type of startRequest is not array" + details:call.arguments]); + return; + } + NSMutableDictionary *properties = [NSMutableDictionary new]; + properties[SKReceiptPropertyIsExpired] = arguments[@"isExpired"]; + properties[SKReceiptPropertyIsRevoked] = arguments[@"isRevoked"]; + properties[SKReceiptPropertyIsVolumePurchase] = arguments[@"isVolumePurchase"]; + request = [self getRefreshReceiptRequest:properties]; + } else { + request = [self getRefreshReceiptRequest:nil]; + } + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + [self.requestHandlers addObject:handler]; + __weak typeof(self) weakSelf = self; + [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + result([FlutterError errorWithCode:@"storekit_refreshreceiptrequest_platform_error" + message:error.localizedDescription + details:error.description]); + return; + } + result(nil); + [weakSelf.requestHandlers removeObject:handler]; + }]; +} + +#pragma mark - delegates: + +- (void)handleTransactionsUpdated:(NSArray *)transactions { + NSMutableArray *maps = [NSMutableArray new]; + for (SKPaymentTransaction *transaction in transactions) { + [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; + } + [self.callbackChannel invokeMethod:@"updatedTransactions" arguments:maps]; +} + +- (void)handleTransactionsRemoved:(NSArray *)transactions { + NSMutableArray *maps = [NSMutableArray new]; + for (SKPaymentTransaction *transaction in transactions) { + [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; + } + [self.callbackChannel invokeMethod:@"removedTransactions" arguments:maps]; +} + +- (void)handleTransactionRestoreFailed:(NSError *)error { + [self.callbackChannel invokeMethod:@"restoreCompletedTransactionsFailed" + arguments:[FIAObjectTranslator getMapFromNSError:error]]; +} + +- (void)restoreCompletedTransactionsFinished { + [self.callbackChannel invokeMethod:@"paymentQueueRestoreCompletedTransactionsFinished" + arguments:nil]; +} + +- (void)updatedDownloads:(NSArray *)downloads { + NSLog(@"Received an updatedDownloads callback, but downloads are not supported."); +} + +- (BOOL)shouldAddStorePayment:(SKPayment *)payment product:(SKProduct *)product { + // We always return NO here. And we send the message to dart to process the payment; and we will + // have a interception method that deciding if the payment should be processed (implemented by the + // programmer). + [self.productsCache setObject:product forKey:product.productIdentifier]; + [self.callbackChannel invokeMethod:@"shouldAddStorePayment" + arguments:@{ + @"payment" : [FIAObjectTranslator getMapFromSKPayment:payment], + @"product" : [FIAObjectTranslator getMapFromSKProduct:product] + }]; + return NO; +} + +#pragma mark - dependency injection (for unit testing) + +- (SKProductsRequest *)getProductRequestWithIdentifiers:(NSSet *)identifiers { + return [[SKProductsRequest alloc] initWithProductIdentifiers:identifiers]; +} + +- (SKProduct *)getProduct:(NSString *)productID { + return [self.productsCache objectForKey:productID]; +} + +- (SKReceiptRefreshRequest *)getRefreshReceiptRequest:(NSDictionary *)properties { + return [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:properties]; +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/InAppPurchasePluginTest.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/InAppPurchasePluginTest.m new file mode 100644 index 000000000000..cb00cbc2a43e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/InAppPurchasePluginTest.m @@ -0,0 +1,304 @@ +// 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 +#import +#import "FIAPaymentQueueHandler.h" +#import "Stubs.h" + +@import in_app_purchase; + +@interface InAppPurchasePluginTest : XCTestCase + +@property(strong, nonatomic) InAppPurchasePlugin* plugin; + +@end + +@implementation InAppPurchasePluginTest + +- (void)setUp { + self.plugin = + [[InAppPurchasePluginStub alloc] initWithReceiptManager:[FIAPReceiptManagerStub new]]; +} + +- (void)tearDown { +} + +- (void)testInvalidMethodCall { + XCTestExpectation* expectation = + [self expectationWithDescription:@"expect result to be not implemented"]; + FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"invalid" arguments:NULL]; + __block id result; + [self.plugin handleMethodCall:call + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(result, FlutterMethodNotImplemented); +} + +- (void)testCanMakePayments { + XCTestExpectation* expectation = [self expectationWithDescription:@"expect result to be YES"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue canMakePayments:]" + arguments:NULL]; + __block id result; + [self.plugin handleMethodCall:call + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(result, [NSNumber numberWithBool:YES]); +} + +- (void)testGetProductResponse { + XCTestExpectation* expectation = + [self expectationWithDescription:@"expect response contains 1 item"]; + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin startProductRequest:result:]" + arguments:@[ @"123" ]]; + __block id result; + [self.plugin handleMethodCall:call + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssert([result isKindOfClass:[NSDictionary class]]); + NSArray* resultArray = [result objectForKey:@"products"]; + XCTAssertEqual(resultArray.count, 1); + XCTAssertTrue([resultArray.firstObject[@"productIdentifier"] isEqualToString:@"123"]); +} + +- (void)testAddPaymentFailure { + XCTestExpectation* expectation = + [self expectationWithDescription:@"result should return failed state"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : @YES, + }]; + SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; + queue.testState = SKPaymentTransactionStateFailed; + __block SKPaymentTransaction* transactionForUpdateBlock; + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray* _Nonnull transactions) { + SKPaymentTransaction* transaction = transactions[0]; + if (transaction.transactionState == SKPaymentTransactionStateFailed) { + transactionForUpdateBlock = transaction; + [expectation fulfill]; + } + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStateFailed); +} + +- (void)testAddPaymentSuccessWithMockQueue { + XCTestExpectation* expectation = + [self expectationWithDescription:@"result should return success state"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : @YES, + }]; + SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; + queue.testState = SKPaymentTransactionStatePurchased; + __block SKPaymentTransaction* transactionForUpdateBlock; + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray* _Nonnull transactions) { + SKPaymentTransaction* transaction = transactions[0]; + if (transaction.transactionState == SKPaymentTransactionStatePurchased) { + transactionForUpdateBlock = transaction; + [expectation fulfill]; + } + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); +} + +- (void)testAddPaymentWithNullSandboxArgument { + XCTestExpectation* expectation = + [self expectationWithDescription:@"result should return success state"]; + XCTestExpectation* simulatesAskToBuyInSandboxExpectation = + [self expectationWithDescription:@"payment isn't simulatesAskToBuyInSandbox"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : [NSNull null], + }]; + SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; + queue.testState = SKPaymentTransactionStatePurchased; + __block SKPaymentTransaction* transactionForUpdateBlock; + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray* _Nonnull transactions) { + SKPaymentTransaction* transaction = transactions[0]; + if (transaction.transactionState == SKPaymentTransactionStatePurchased) { + transactionForUpdateBlock = transaction; + [expectation fulfill]; + } + if (@available(iOS 8.3, *)) { + if (!transaction.payment.simulatesAskToBuyInSandbox) { + [simulatesAskToBuyInSandboxExpectation fulfill]; + } + } else { + [simulatesAskToBuyInSandboxExpectation fulfill]; + } + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + [self waitForExpectations:@[ expectation, simulatesAskToBuyInSandboxExpectation ] timeout:5]; + XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); +} + +- (void)testRestoreTransactions { + XCTestExpectation* expectation = + [self expectationWithDescription:@"result successfully restore transactions"]; + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin restoreTransactions:result:]" + arguments:nil]; + SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; + queue.testState = SKPaymentTransactionStatePurchased; + __block BOOL callbackInvoked = NO; + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray* _Nonnull transactions) { + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:^() { + callbackInvoked = YES; + [expectation fulfill]; + } + shouldAddStorePayment:nil + updatedDownloads:nil]; + [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue(callbackInvoked); +} + +- (void)testRetrieveReceiptData { + XCTestExpectation* expectation = [self expectationWithDescription:@"receipt data retrieved"]; + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" + arguments:nil]; + __block NSDictionary* result; + [self.plugin handleMethodCall:call + result:^(id r) { + result = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + NSLog(@"%@", result); + XCTAssertNotNil(result); +} + +- (void)testRefreshReceiptRequest { + XCTestExpectation* expectation = [self expectationWithDescription:@"expect success"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin refreshReceipt:result:]" + arguments:nil]; + __block BOOL result = NO; + [self.plugin handleMethodCall:call + result:^(id r) { + result = YES; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue(result); +} + +- (void)testPresentCodeRedemptionSheet { + XCTestExpectation* expectation = + [self expectationWithDescription:@"expect successfully present Code Redemption Sheet"]; + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]" + arguments:nil]; + __block BOOL callbackInvoked = NO; + [self.plugin handleMethodCall:call + result:^(id r) { + callbackInvoked = YES; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue(callbackInvoked); +} + +- (void)testGetPendingTransactions { + XCTestExpectation* expectation = [self expectationWithDescription:@"expect success"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue transactions]" arguments:nil]; + SKPaymentQueue* mockQueue = OCMClassMock(SKPaymentQueue.class); + NSDictionary* transactionMap = @{ + @"transactionIdentifier" : [NSNull null], + @"transactionState" : @(SKPaymentTransactionStatePurchasing), + @"payment" : [NSNull null], + @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" + code:123 + userInfo:@{}]], + @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), + @"originalTransaction" : [NSNull null], + }; + OCMStub(mockQueue.transactions).andReturn(@[ [[SKPaymentTransactionStub alloc] + initWithMap:transactionMap] ]); + + __block NSArray* resultArray; + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:mockQueue + transactionsUpdated:nil + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:nil + updatedDownloads:nil]; + [self.plugin handleMethodCall:call + result:^(id r) { + resultArray = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqualObjects(resultArray, @[ transactionMap ]); +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/PaymentQueueTest.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/PaymentQueueTest.m new file mode 100644 index 000000000000..c335fa3ef307 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/PaymentQueueTest.m @@ -0,0 +1,212 @@ +// 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 +#import "Stubs.h" + +@import in_app_purchase; + +@interface PaymentQueueTest : XCTestCase + +@property(strong, nonatomic) NSDictionary *periodMap; +@property(strong, nonatomic) NSDictionary *discountMap; +@property(strong, nonatomic) NSDictionary *productMap; +@property(strong, nonatomic) NSDictionary *productResponseMap; + +@end + +@implementation PaymentQueueTest + +- (void)setUp { + self.periodMap = @{@"numberOfUnits" : @(0), @"unit" : @(0)}; + self.discountMap = @{ + @"price" : @1.0, + @"currencyCode" : @"USD", + @"numberOfPeriods" : @1, + @"subscriptionPeriod" : self.periodMap, + @"paymentMode" : @1 + }; + self.productMap = @{ + @"price" : @1.0, + @"currencyCode" : @"USD", + @"productIdentifier" : @"123", + @"localizedTitle" : @"title", + @"localizedDescription" : @"des", + @"subscriptionPeriod" : self.periodMap, + @"introductoryPrice" : self.discountMap, + @"subscriptionGroupIdentifier" : @"com.group" + }; + self.productResponseMap = + @{@"products" : @[ self.productMap ], @"invalidProductIdentifiers" : [NSNull null]}; +} + +- (void)testTransactionPurchased { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get purchased transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStatePurchased; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:handler]; + SKPayment *payment = + [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler addPayment:payment]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(tran.transactionState, SKPaymentTransactionStatePurchased); + XCTAssertEqual(tran.transactionIdentifier, @"fakeID"); +} + +- (void)testTransactionFailed { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get failed transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStateFailed; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:handler]; + SKPayment *payment = + [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler addPayment:payment]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateFailed); + XCTAssertEqual(tran.transactionIdentifier, nil); +} + +- (void)testTransactionRestored { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get restored transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStateRestored; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:handler]; + SKPayment *payment = + [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler addPayment:payment]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateRestored); + XCTAssertEqual(tran.transactionIdentifier, @"fakeID"); +} + +- (void)testTransactionPurchasing { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get purchasing transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStatePurchasing; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:handler]; + SKPayment *payment = + [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler addPayment:payment]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(tran.transactionState, SKPaymentTransactionStatePurchasing); + XCTAssertEqual(tran.transactionIdentifier, nil); +} + +- (void)testTransactionDeferred { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get deffered transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStateDeferred; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:handler]; + SKPayment *payment = + [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler addPayment:payment]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateDeferred); + XCTAssertEqual(tran.transactionIdentifier, nil); +} + +- (void)testFinishTransaction { + XCTestExpectation *expectation = + [self expectationWithDescription:@"handler.transactions should be empty."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStateDeferred; + __block FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + XCTAssertEqual(transactions.count, 1); + SKPaymentTransaction *transaction = transactions[0]; + [handler finishTransaction:transaction]; + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + XCTAssertEqual(transactions.count, 1); + [expectation fulfill]; + } + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:handler]; + SKPayment *payment = + [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler addPayment:payment]; + [self waitForExpectations:@[ expectation ] timeout:5]; +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/ProductRequestHandlerTest.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/ProductRequestHandlerTest.m new file mode 100644 index 000000000000..19f5848b7168 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/ProductRequestHandlerTest.m @@ -0,0 +1,89 @@ +// 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 +#import "Stubs.h" + +@import in_app_purchase; + +#pragma tests start here + +@interface RequestHandlerTest : XCTestCase + +@end + +@implementation RequestHandlerTest + +- (void)testRequestHandlerWithProductRequestSuccess { + SKProductRequestStub *request = + [[SKProductRequestStub alloc] initWithProductIdentifiers:[NSSet setWithArray:@[ @"123" ]]]; + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get response with 1 product"]; + __block SKProductsResponse *response; + [handler + startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *error) { + response = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNotNil(response); + XCTAssertEqual(response.products.count, 1); + SKProduct *product = response.products.firstObject; + XCTAssertTrue([product.productIdentifier isEqualToString:@"123"]); +} + +- (void)testRequestHandlerWithProductRequestFailure { + SKProductRequestStub *request = [[SKProductRequestStub alloc] + initWithFailureError:[NSError errorWithDomain:@"test" code:123 userInfo:@{}]]; + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get response with 1 product"]; + __block NSError *error; + __block SKProductsResponse *response; + [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *e) { + error = e; + response = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNotNil(error); + XCTAssertEqual(error.domain, @"test"); + XCTAssertNil(response); +} + +- (void)testRequestHandlerWithRefreshReceiptSuccess { + SKReceiptRefreshRequestStub *request = + [[SKReceiptRefreshRequestStub alloc] initWithReceiptProperties:nil]; + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + XCTestExpectation *expectation = [self expectationWithDescription:@"expect no error"]; + __block NSError *e; + [handler + startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *error) { + e = error; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNil(e); +} + +- (void)testRequestHandlerWithRefreshReceiptFailure { + SKReceiptRefreshRequestStub *request = [[SKReceiptRefreshRequestStub alloc] + initWithFailureError:[NSError errorWithDomain:@"test" code:123 userInfo:@{}]]; + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + XCTestExpectation *expectation = [self expectationWithDescription:@"expect error"]; + __block NSError *error; + __block SKProductsResponse *response; + [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *e) { + error = e; + response = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNotNil(error); + XCTAssertEqual(error.domain, @"test"); + XCTAssertNil(response); +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/Stubs.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/Stubs.h new file mode 100644 index 000000000000..e07cc3f5a147 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/Stubs.h @@ -0,0 +1,62 @@ +// 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 +#import + +@import in_app_purchase; + +NS_ASSUME_NONNULL_BEGIN +API_AVAILABLE(ios(11.2), macos(10.13.2)) +@interface SKProductSubscriptionPeriodStub : SKProductSubscriptionPeriod +- (instancetype)initWithMap:(NSDictionary *)map; +@end + +API_AVAILABLE(ios(11.2), macos(10.13.2)) +@interface SKProductDiscountStub : SKProductDiscount +- (instancetype)initWithMap:(NSDictionary *)map; +@end + +@interface SKProductStub : SKProduct +- (instancetype)initWithMap:(NSDictionary *)map; +@end + +@interface SKProductRequestStub : SKProductsRequest +- (instancetype)initWithProductIdentifiers:(NSSet *)productIdentifiers; +- (instancetype)initWithFailureError:(NSError *)error; +@end + +@interface SKProductsResponseStub : SKProductsResponse +- (instancetype)initWithMap:(NSDictionary *)map; +@end + +@interface InAppPurchasePluginStub : InAppPurchasePlugin +@end + +@interface SKPaymentQueueStub : SKPaymentQueue +@property(assign, nonatomic) SKPaymentTransactionState testState; +@end + +@interface SKPaymentTransactionStub : SKPaymentTransaction +- (instancetype)initWithMap:(NSDictionary *)map; +- (instancetype)initWithState:(SKPaymentTransactionState)state; +- (instancetype)initWithState:(SKPaymentTransactionState)state payment:(SKPayment *)payment; +@end + +@interface SKMutablePaymentStub : SKMutablePayment +- (instancetype)initWithMap:(NSDictionary *)map; +@end + +@interface NSErrorStub : NSError +- (instancetype)initWithMap:(NSDictionary *)map; +@end + +@interface FIAPReceiptManagerStub : FIAPReceiptManager +@end + +@interface SKReceiptRefreshRequestStub : SKReceiptRefreshRequest +- (instancetype)initWithFailureError:(NSError *)error; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/Stubs.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/Stubs.m new file mode 100644 index 000000000000..66610a88a77d --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/Stubs.m @@ -0,0 +1,290 @@ +// 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 "Stubs.h" + +@implementation SKProductSubscriptionPeriodStub + +- (instancetype)initWithMap:(NSDictionary *)map { + self = [super init]; + if (self) { + [self setValue:map[@"numberOfUnits"] ?: @(0) forKey:@"numberOfUnits"]; + [self setValue:map[@"unit"] ?: @(0) forKey:@"unit"]; + } + return self; +} + +@end + +@implementation SKProductDiscountStub + +- (instancetype)initWithMap:(NSDictionary *)map { + self = [super init]; + if (self) { + [self setValue:[[NSDecimalNumber alloc] initWithString:map[@"price"]] ?: [NSNull null] + forKey:@"price"]; + NSLocale *locale = NSLocale.systemLocale; + [self setValue:locale ?: [NSNull null] forKey:@"priceLocale"]; + [self setValue:map[@"numberOfPeriods"] ?: @(0) forKey:@"numberOfPeriods"]; + SKProductSubscriptionPeriodStub *subscriptionPeriodSub = + [[SKProductSubscriptionPeriodStub alloc] initWithMap:map[@"subscriptionPeriod"]]; + [self setValue:subscriptionPeriodSub forKey:@"subscriptionPeriod"]; + [self setValue:map[@"paymentMode"] ?: @(0) forKey:@"paymentMode"]; + } + return self; +} + +@end + +@implementation SKProductStub + +- (instancetype)initWithMap:(NSDictionary *)map { + self = [super init]; + if (self) { + [self setValue:map[@"productIdentifier"] ?: [NSNull null] forKey:@"productIdentifier"]; + [self setValue:map[@"localizedDescription"] ?: [NSNull null] forKey:@"localizedDescription"]; + [self setValue:map[@"localizedTitle"] ?: [NSNull null] forKey:@"localizedTitle"]; + [self setValue:map[@"downloadable"] ?: @NO forKey:@"downloadable"]; + [self setValue:[[NSDecimalNumber alloc] initWithString:map[@"price"]] ?: [NSNull null] + forKey:@"price"]; + NSLocale *locale = NSLocale.systemLocale; + [self setValue:locale ?: [NSNull null] forKey:@"priceLocale"]; + [self setValue:map[@"downloadContentLengths"] ?: @(0) forKey:@"downloadContentLengths"]; + if (@available(iOS 11.2, *)) { + SKProductSubscriptionPeriodStub *period = + [[SKProductSubscriptionPeriodStub alloc] initWithMap:map[@"subscriptionPeriod"]]; + [self setValue:period ?: [NSNull null] forKey:@"subscriptionPeriod"]; + SKProductDiscountStub *discount = + [[SKProductDiscountStub alloc] initWithMap:map[@"introductoryPrice"]]; + [self setValue:discount ?: [NSNull null] forKey:@"introductoryPrice"]; + [self setValue:map[@"subscriptionGroupIdentifier"] ?: [NSNull null] + forKey:@"subscriptionGroupIdentifier"]; + } + } + return self; +} + +- (instancetype)initWithProductID:(NSString *)productIdentifier { + self = [super init]; + if (self) { + [self setValue:productIdentifier forKey:@"productIdentifier"]; + } + return self; +} + +@end + +@interface SKProductRequestStub () + +@property(strong, nonatomic) NSSet *identifers; +@property(strong, nonatomic) NSError *error; + +@end + +@implementation SKProductRequestStub + +- (instancetype)initWithProductIdentifiers:(NSSet *)productIdentifiers { + self = [super initWithProductIdentifiers:productIdentifiers]; + self.identifers = productIdentifiers; + return self; +} + +- (instancetype)initWithFailureError:(NSError *)error { + self = [super init]; + self.error = error; + return self; +} + +- (void)start { + NSMutableArray *productArray = [NSMutableArray new]; + for (NSString *identifier in self.identifers) { + [productArray addObject:@{@"productIdentifier" : identifier}]; + } + SKProductsResponseStub *response = + [[SKProductsResponseStub alloc] initWithMap:@{@"products" : productArray}]; + if (self.error) { + [self.delegate request:self didFailWithError:self.error]; + } else { + [self.delegate productsRequest:self didReceiveResponse:response]; + } +} + +@end + +@implementation SKProductsResponseStub + +- (instancetype)initWithMap:(NSDictionary *)map { + self = [super init]; + if (self) { + NSMutableArray *products = [NSMutableArray new]; + for (NSDictionary *productMap in map[@"products"]) { + SKProductStub *product = [[SKProductStub alloc] initWithMap:productMap]; + [products addObject:product]; + } + [self setValue:products forKey:@"products"]; + } + return self; +} + +@end + +@interface InAppPurchasePluginStub () + +@end + +@implementation InAppPurchasePluginStub + +- (SKProductRequestStub *)getProductRequestWithIdentifiers:(NSSet *)identifiers { + return [[SKProductRequestStub alloc] initWithProductIdentifiers:identifiers]; +} + +- (SKProduct *)getProduct:(NSString *)productID { + return [[SKProductStub alloc] initWithProductID:productID]; +} + +- (SKReceiptRefreshRequestStub *)getRefreshReceiptRequest:(NSDictionary *)properties { + return [[SKReceiptRefreshRequestStub alloc] initWithReceiptProperties:properties]; +} + +@end + +@interface SKPaymentQueueStub () + +@property(strong, nonatomic) id observer; + +@end + +@implementation SKPaymentQueueStub + +- (void)addTransactionObserver:(id)observer { + self.observer = observer; +} + +- (void)addPayment:(SKPayment *)payment { + SKPaymentTransactionStub *transaction = + [[SKPaymentTransactionStub alloc] initWithState:self.testState payment:payment]; + [self.observer paymentQueue:self updatedTransactions:@[ transaction ]]; +} + +- (void)restoreCompletedTransactions { + if ([self.observer + respondsToSelector:@selector(paymentQueueRestoreCompletedTransactionsFinished:)]) { + [self.observer paymentQueueRestoreCompletedTransactionsFinished:self]; + } +} + +- (void)finishTransaction:(SKPaymentTransaction *)transaction { + if ([self.observer respondsToSelector:@selector(paymentQueue:removedTransactions:)]) { + [self.observer paymentQueue:self removedTransactions:@[ transaction ]]; + } +} + +@end + +@implementation SKPaymentTransactionStub { + SKPayment *_payment; +} + +- (instancetype)initWithID:(NSString *)identifier { + self = [super init]; + if (self) { + [self setValue:identifier forKey:@"transactionIdentifier"]; + } + return self; +} + +- (instancetype)initWithMap:(NSDictionary *)map { + self = [super init]; + if (self) { + [self setValue:map[@"transactionIdentifier"] forKey:@"transactionIdentifier"]; + [self setValue:map[@"transactionState"] forKey:@"transactionState"]; + if (![map[@"originalTransaction"] isKindOfClass:[NSNull class]] && + map[@"originalTransaction"]) { + [self setValue:[[SKPaymentTransactionStub alloc] initWithMap:map[@"originalTransaction"]] + forKey:@"originalTransaction"]; + } + [self setValue:map[@"error"] ? [[NSErrorStub alloc] initWithMap:map[@"error"]] : [NSNull null] + forKey:@"error"]; + [self setValue:[NSDate dateWithTimeIntervalSince1970:[map[@"transactionTimeStamp"] doubleValue]] + forKey:@"transactionDate"]; + } + return self; +} + +- (instancetype)initWithState:(SKPaymentTransactionState)state { + self = [super init]; + if (self) { + // Only purchased and restored transactions have transactionIdentifier: + // https://developer.apple.com/documentation/storekit/skpaymenttransaction/1411288-transactionidentifier?language=objc + if (state == SKPaymentTransactionStatePurchased || state == SKPaymentTransactionStateRestored) { + [self setValue:@"fakeID" forKey:@"transactionIdentifier"]; + } + [self setValue:@(state) forKey:@"transactionState"]; + } + return self; +} + +- (instancetype)initWithState:(SKPaymentTransactionState)state payment:(SKPayment *)payment { + self = [super init]; + if (self) { + // Only purchased and restored transactions have transactionIdentifier: + // https://developer.apple.com/documentation/storekit/skpaymenttransaction/1411288-transactionidentifier?language=objc + if (state == SKPaymentTransactionStatePurchased || state == SKPaymentTransactionStateRestored) { + [self setValue:@"fakeID" forKey:@"transactionIdentifier"]; + } + [self setValue:@(state) forKey:@"transactionState"]; + _payment = payment; + } + return self; +} + +- (SKPayment *)payment { + return _payment; +} + +@end + +@implementation NSErrorStub + +- (instancetype)initWithMap:(NSDictionary *)map { + return [self initWithDomain:[map objectForKey:@"domain"] + code:[[map objectForKey:@"code"] integerValue] + userInfo:[map objectForKey:@"userInfo"]]; +} + +@end + +@implementation FIAPReceiptManagerStub : FIAPReceiptManager + +- (NSData *)getReceiptData:(NSURL *)url { + NSString *originalString = [NSString stringWithFormat:@"test"]; + return [[NSData alloc] initWithBase64EncodedString:originalString options:kNilOptions]; +} + +@end + +@implementation SKReceiptRefreshRequestStub { + NSError *_error; +} + +- (instancetype)initWithReceiptProperties:(NSDictionary *)properties { + self = [super initWithReceiptProperties:properties]; + return self; +} + +- (instancetype)initWithFailureError:(NSError *)error { + self = [super init]; + _error = error; + return self; +} + +- (void)start { + if (_error) { + [self.delegate request:self didFailWithError:_error]; + } else { + [self.delegate requestDidFinish:self]; + } +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/TranslatorTest.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/TranslatorTest.m new file mode 100644 index 000000000000..550d1fc341c6 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/TranslatorTest.m @@ -0,0 +1,147 @@ +// 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 +#import "Stubs.h" + +@import in_app_purchase; + +@interface TranslatorTest : XCTestCase + +@property(strong, nonatomic) NSDictionary *periodMap; +@property(strong, nonatomic) NSDictionary *discountMap; +@property(strong, nonatomic) NSMutableDictionary *productMap; +@property(strong, nonatomic) NSDictionary *productResponseMap; +@property(strong, nonatomic) NSDictionary *paymentMap; +@property(strong, nonatomic) NSDictionary *transactionMap; +@property(strong, nonatomic) NSDictionary *errorMap; +@property(strong, nonatomic) NSDictionary *localeMap; + +@end + +@implementation TranslatorTest + +- (void)setUp { + self.periodMap = @{@"numberOfUnits" : @(0), @"unit" : @(0)}; + self.discountMap = @{ + @"price" : @"1", + @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], + @"numberOfPeriods" : @1, + @"subscriptionPeriod" : self.periodMap, + @"paymentMode" : @1 + }; + + self.productMap = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"price" : @"1", + @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], + @"productIdentifier" : @"123", + @"localizedTitle" : @"title", + @"localizedDescription" : @"des", + }]; + if (@available(iOS 11.2, *)) { + self.productMap[@"subscriptionPeriod"] = self.periodMap; + self.productMap[@"introductoryPrice"] = self.discountMap; + } + + if (@available(iOS 12.0, *)) { + self.productMap[@"subscriptionGroupIdentifier"] = @"com.group"; + } + + self.productResponseMap = + @{@"products" : @[ self.productMap ], @"invalidProductIdentifiers" : @[]}; + self.paymentMap = @{ + @"productIdentifier" : @"123", + @"requestData" : @"abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh", + @"quantity" : @(2), + @"applicationUsername" : @"app user name", + @"simulatesAskToBuyInSandbox" : @(NO) + }; + NSDictionary *originalTransactionMap = @{ + @"transactionIdentifier" : @"567", + @"transactionState" : @(SKPaymentTransactionStatePurchasing), + @"payment" : [NSNull null], + @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" + code:123 + userInfo:@{}]], + @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), + @"originalTransaction" : [NSNull null], + }; + self.transactionMap = @{ + @"transactionIdentifier" : @"567", + @"transactionState" : @(SKPaymentTransactionStatePurchasing), + @"payment" : [NSNull null], + @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" + code:123 + userInfo:@{}]], + @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), + @"originalTransaction" : originalTransactionMap, + }; + self.errorMap = @{ + @"code" : @(123), + @"domain" : @"test_domain", + @"userInfo" : @{ + @"key" : @"value", + } + }; +} + +- (void)testSKProductSubscriptionPeriodStubToMap { + if (@available(iOS 11.2, *)) { + SKProductSubscriptionPeriodStub *period = + [[SKProductSubscriptionPeriodStub alloc] initWithMap:self.periodMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKProductSubscriptionPeriod:period]; + XCTAssertEqualObjects(map, self.periodMap); + } +} + +- (void)testSKProductDiscountStubToMap { + if (@available(iOS 11.2, *)) { + SKProductDiscountStub *discount = [[SKProductDiscountStub alloc] initWithMap:self.discountMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKProductDiscount:discount]; + XCTAssertEqualObjects(map, self.discountMap); + } +} + +- (void)testProductToMap { + SKProductStub *product = [[SKProductStub alloc] initWithMap:self.productMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKProduct:product]; + XCTAssertEqualObjects(map, self.productMap); +} + +- (void)testProductResponseToMap { + SKProductsResponseStub *response = + [[SKProductsResponseStub alloc] initWithMap:self.productResponseMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKProductsResponse:response]; + XCTAssertEqualObjects(map, self.productResponseMap); +} + +- (void)testPaymentToMap { + SKMutablePayment *payment = [FIAObjectTranslator getSKMutablePaymentFromMap:self.paymentMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKPayment:payment]; + XCTAssertEqualObjects(map, self.paymentMap); +} + +- (void)testPaymentTransactionToMap { + // payment is not KVC, cannot test payment field. + SKPaymentTransactionStub *paymentTransaction = + [[SKPaymentTransactionStub alloc] initWithMap:self.transactionMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKPaymentTransaction:paymentTransaction]; + XCTAssertEqualObjects(map, self.transactionMap); +} + +- (void)testError { + NSErrorStub *error = [[NSErrorStub alloc] initWithMap:self.errorMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromNSError:error]; + XCTAssertEqualObjects(map, self.errorMap); +} + +- (void)testLocaleToMap { + if (@available(iOS 10.0, *)) { + NSLocale *system = NSLocale.systemLocale; + NSDictionary *map = [FIAObjectTranslator getMapFromNSLocale:system]; + XCTAssertEqualObjects(map[@"currencySymbol"], system.currencySymbol); + } +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase.podspec b/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase.podspec new file mode 100644 index 000000000000..8da9d7894380 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase.podspec @@ -0,0 +1,27 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'in_app_purchase' + s.version = '0.0.1' + s.summary = 'Flutter In App Purchase' + s.description = <<-DESC +A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store. +Downloaded by pub (not CocoaPods). + DESC + s.homepage = 'https://github.com/flutter/plugins' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/in_app_purchase' } + s.documentation_url = 'https://pub.dev/packages/in_app_purchase' + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + s.platform = :ios, '8.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS' => 'armv7 arm64 x86_64' } + + s.test_spec 'Tests' do |test_spec| + test_spec.source_files = 'Tests/**/*' + test_spec.dependency 'OCMock','3.5' + end +end diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/in_app_purchase_ios.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/in_app_purchase_ios.dart new file mode 100644 index 000000000000..1eebc1dcb926 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/in_app_purchase_ios.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_ios_platform.dart'; +export 'src/models/models.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/channel.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/channel.dart new file mode 100644 index 000000000000..f8ab4d48be7e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/channel.dart @@ -0,0 +1,9 @@ +// 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/services.dart'; + +/// Method channel for the plugin's platform<-->Dart calls. +const MethodChannel channel = + MethodChannel('plugins.flutter.io/in_app_purchase'); diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart new file mode 100644 index 000000000000..709471334006 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart @@ -0,0 +1,236 @@ +// 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:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +import '../in_app_purchase_ios.dart'; +import '../in_app_purchase_ios.dart'; +import '../store_kit_wrappers.dart'; + +/// [IAPError.code] code for failed purchases. +final String kPurchaseErrorCode = 'purchase_error'; + +/// [IAPError.code] code used when a query for previouys transaction has failed. +final String kRestoredPurchaseErrorCode = 'restore_transactions_failed'; + +/// [IAPError.code] code used when a consuming a purchased item fails. +final String kConsumptionFailedErrorCode = 'consume_purchase_failed'; + +/// Indicates store front is Apple AppStore. +final String kIAPSource = 'app_store'; + +/// An [InAppPurchasePlatform] that wraps StoreKit. +/// +/// This translates various `StoreKit` calls and responses into the +/// generic plugin API. +class InAppPurchaseIosPlatform implements InAppPurchasePlatform { + /// Returns the singleton instance of the [InAppPurchaseIosPlatform] that should be + /// used across the app. + static InAppPurchaseIosPlatform get instance => _getOrCreateInstance(); + static InAppPurchaseIosPlatform? _instance; + static late SKPaymentQueueWrapper _skPaymentQueueWrapper; + static late _TransactionObserver _observer; + + /// Creates an [InAppPurchaseIosPlatform] object. + /// + /// This constructor should only be used for testing, for any other purpose + /// get the connection from the [instance] getter. + @visibleForTesting + InAppPurchaseIosPlatform(); + + Stream> get purchaseStream => + _observer.purchaseUpdatedController.stream; + + /// Callback handler for transaction status changes. + @visibleForTesting + static SKTransactionObserverWrapper get observer => _observer; + + static InAppPurchaseIosPlatform _getOrCreateInstance() { + if (_instance != null) { + return _instance!; + } + + _instance = InAppPurchaseIosPlatform(); + _skPaymentQueueWrapper = SKPaymentQueueWrapper(); + _observer = _TransactionObserver(StreamController.broadcast()); + _skPaymentQueueWrapper.setTransactionObserver(observer); + return _instance!; + } + + @override + Future isAvailable() => SKPaymentQueueWrapper.canMakePayments(); + + @override + Future buyNonConsumable({required PurchaseParam purchaseParam}) async { + if (!(purchaseParam is AppStorePurchaseParam)) { + throw ArgumentError( + 'On iOS, the `purchaseParam` should always be of type `AppStorePurchaseParam`.', + ); + } + + await _skPaymentQueueWrapper.addPayment(SKPaymentWrapper( + productIdentifier: purchaseParam.productDetails.id, + quantity: 1, + applicationUsername: purchaseParam.applicationUserName, + simulatesAskToBuyInSandbox: purchaseParam.simulatesAskToBuyInSandbox, + requestData: null)); + + return true; // There's no error feedback from iOS here to return. + } + + @override + Future buyConsumable( + {required PurchaseParam purchaseParam, bool autoConsume = true}) { + assert(autoConsume == true, 'On iOS, we should always auto consume'); + return buyNonConsumable(purchaseParam: purchaseParam); + } + + @override + Future completePurchase(PurchaseDetails purchase) { + if (!(purchase is AppStorePurchaseDetails)) { + throw ArgumentError( + 'On iOS, the `purchase` should always be of type `AppStorePurchaseDetails`.', + ); + } + + return _skPaymentQueueWrapper.finishTransaction( + purchase.skPaymentTransaction, + ); + } + + @override + Future restorePurchases({String? applicationUserName}) async { + return _observer + .restoreTransactions( + queue: _skPaymentQueueWrapper, + applicationUserName: applicationUserName) + .whenComplete(() => _observer.cleanUpRestoredTransactions()); + } + + /// Query the product detail list. + /// + /// This method only returns [ProductDetailsResponse]. + /// To get detailed Store Kit product list, use [SkProductResponseWrapper.startProductRequest] + /// to get the [SKProductResponseWrapper]. + @override + Future queryProductDetails( + Set identifiers) async { + final SKRequestMaker requestMaker = SKRequestMaker(); + SkProductResponseWrapper response; + PlatformException? exception; + try { + response = await requestMaker.startProductRequest(identifiers.toList()); + } on PlatformException catch (e) { + exception = e; + response = SkProductResponseWrapper( + products: [], invalidProductIdentifiers: identifiers.toList()); + } + List productDetails = []; + if (response.products != null) { + productDetails = response.products + .map((SKProductWrapper productWrapper) => + AppStoreProductDetails.fromSKProduct(productWrapper)) + .toList(); + } + List invalidIdentifiers = response.invalidProductIdentifiers; + if (productDetails.isEmpty) { + invalidIdentifiers = identifiers.toList(); + } + ProductDetailsResponse productDetailsResponse = ProductDetailsResponse( + productDetails: productDetails, + notFoundIDs: invalidIdentifiers, + error: exception == null + ? null + : IAPError( + source: kIAPSource, + code: exception.code, + message: exception.message ?? '', + details: exception.details), + ); + return productDetailsResponse; + } + + // TODO(mvanbeusekom): Move into `InAppPurchaseIosAddition` class... + /* + @override + Future refreshPurchaseVerificationData() async { + await SKRequestMaker().startRefreshReceiptRequest(); + final String? receipt = await SKReceiptManager.retrieveReceiptData(); + if (receipt == null) { + return null; + } + return PurchaseVerificationData( + localVerificationData: receipt, + serverVerificationData: receipt, + source: kIAPSource); + } + + @override + Future presentCodeRedemptionSheet() { + return _skPaymentQueueWrapper.presentCodeRedemptionSheet(); + } + */ +} + +class _TransactionObserver implements SKTransactionObserverWrapper { + final StreamController> purchaseUpdatedController; + + Completer>? _restoreCompleter; + late String _receiptData; + + _TransactionObserver(this.purchaseUpdatedController); + + Future restoreTransactions( + {required SKPaymentQueueWrapper queue, String? applicationUserName}) { + _restoreCompleter = Completer(); + queue.restoreTransactions(applicationUserName: applicationUserName); + return _restoreCompleter!.future; + } + + void cleanUpRestoredTransactions() { + _restoreCompleter = null; + } + + void updatedTransactions( + {required List transactions}) async { + String receiptData = await getReceiptData(); + purchaseUpdatedController + .add(transactions.map((SKPaymentTransactionWrapper transaction) { + AppStorePurchaseDetails purchaseDetails = + AppStorePurchaseDetails.fromSKTransaction(transaction, receiptData); + return purchaseDetails; + }).toList()); + } + + void removedTransactions( + {required List transactions}) {} + + /// Triggered when there is an error while restoring transactions. + void restoreCompletedTransactionsFailed({required SKError error}) { + _restoreCompleter!.completeError(error); + } + + void paymentQueueRestoreCompletedTransactionsFinished() { + _restoreCompleter!.complete(); + } + + bool shouldAddStorePayment( + {required SKPaymentWrapper payment, required SKProductWrapper product}) { + // In this unified API, we always return true to keep it consistent with the behavior on Google Play. + return true; + } + + Future getReceiptData() async { + try { + _receiptData = await SKReceiptManager.retrieveReceiptData(); + } catch (e) { + _receiptData = ''; + } + return _receiptData; + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/models/app_store_product_details.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/models/app_store_product_details.dart new file mode 100644 index 000000000000..96386c5ef5ad --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/models/app_store_product_details.dart @@ -0,0 +1,47 @@ +// 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/in_app_purchase_platform_interface.dart'; + +import '../../store_kit_wrappers.dart'; + +/// The class represents the information of a product as registered in the Apple +/// AppStore. +class AppStoreProductDetails extends ProductDetails { + /// Creates a new AppStore specific product details object with the provided + /// details. + AppStoreProductDetails({ + required String id, + required String title, + required String description, + required String price, + required double rawPrice, + required String currencyCode, + required this.skProduct, + }) : super( + id: id, + title: title, + description: description, + price: price, + rawPrice: rawPrice, + currencyCode: currencyCode, + ); + + /// Points back to the [SKProductWrapper] object that was used to generate + /// this [AppStoreProductDetails] object. + final SKProductWrapper skProduct; + + /// Generate a [AppStoreProductDetails] object based on an iOS [SKProductWrapper] object. + factory AppStoreProductDetails.fromSKProduct(SKProductWrapper product) { + return AppStoreProductDetails( + id: product.productIdentifier, + title: product.localizedTitle, + description: product.localizedDescription, + price: product.priceLocale.currencySymbol + product.price, + rawPrice: double.parse(product.price), + currencyCode: product.priceLocale.currencyCode, + skProduct: product, + ); + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/models/app_store_purchase_details.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/models/app_store_purchase_details.dart new file mode 100644 index 000000000000..388f7ca07669 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/models/app_store_purchase_details.dart @@ -0,0 +1,66 @@ +// 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/in_app_purchase_platform_interface.dart'; + +import '../../in_app_purchase_ios.dart'; +import '../../store_kit_wrappers.dart'; +import '../store_kit_wrappers/enum_converters.dart'; + +/// The class represents the information of a purchase made with the Apple +/// AppStore. +class AppStorePurchaseDetails extends PurchaseDetails { + /// Creates a new AppStore specific purchase details object with the provided + /// details. + AppStorePurchaseDetails({ + String? purchaseID, + required String productID, + required PurchaseVerificationData verificationData, + required String? transactionDate, + required this.skPaymentTransaction, + }) : super( + productID: productID, + purchaseID: purchaseID, + transactionDate: transactionDate, + verificationData: verificationData, + ); + + /// Points back to the [SKPaymentTransactionWrapper] which was used to + /// generate this [AppStorePurchaseDetails] object. + final SKPaymentTransactionWrapper skPaymentTransaction; + + /// Generate a [AppStorePurchaseDetails] object based on an iOS + /// [SKPaymentTransactionWrapper] object. + factory AppStorePurchaseDetails.fromSKTransaction( + SKPaymentTransactionWrapper transaction, + String base64EncodedReceipt, + ) { + final AppStorePurchaseDetails purchaseDetails = AppStorePurchaseDetails( + productID: transaction.payment.productIdentifier, + purchaseID: transaction.transactionIdentifier, + skPaymentTransaction: transaction, + transactionDate: transaction.transactionTimeStamp != null + ? (transaction.transactionTimeStamp! * 1000).toInt().toString() + : null, + verificationData: PurchaseVerificationData( + localVerificationData: base64EncodedReceipt, + serverVerificationData: base64EncodedReceipt, + source: kIAPSource), + ); + + purchaseDetails.status = SKTransactionStatusConverter() + .toPurchaseStatus(transaction.transactionState); + + if (purchaseDetails.status == PurchaseStatus.error) { + purchaseDetails.error = IAPError( + source: kIAPSource, + code: kPurchaseErrorCode, + message: transaction.error?.domain ?? '', + details: transaction.error?.userInfo, + ); + } + + return purchaseDetails; + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/models/app_store_purchase_param.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/models/app_store_purchase_param.dart new file mode 100644 index 000000000000..b825eeadf5a3 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/models/app_store_purchase_param.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 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +import '../../store_kit_wrappers.dart'; + +/// Apple AppStore specific parameter object for generating a purchase. +class AppStorePurchaseParam extends PurchaseParam { + /// Creates a new [AppStorePurchaseParam] object with the given data. + AppStorePurchaseParam({ + required ProductDetails productDetails, + String? applicationUserName, + this.simulatesAskToBuyInSandbox = false, + }) : super( + productDetails: productDetails, + applicationUserName: applicationUserName, + ); + + /// Set it to `true` to produce an "ask to buy" flow for this payment in the + /// sandbox. + /// + /// See also [SKPaymentWrapper.simulatesAskToBuyInSandbox]. + final bool simulatesAskToBuyInSandbox; +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/models/models.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/models/models.dart new file mode 100644 index 000000000000..fdca6a0fb12f --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/models/models.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. +// +export 'app_store_product_details.dart'; +export 'app_store_purchase_details.dart'; +export 'app_store_purchase_param.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/README.md b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/README.md new file mode 100644 index 000000000000..bd8e5b540ba0 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/README.md @@ -0,0 +1,5 @@ +# store_kit_wrappers + +This exposes Dart endpoints through to the +[StoreKit](https://developer.apple.com/documentation/storekit) APIs. Can be used +as an alternative to [in_app_purchase](../in_app_purchase/README.md). \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart new file mode 100644 index 000000000000..a7235f6252fe --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart @@ -0,0 +1,108 @@ +// 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/in_app_purchase_platform_interface.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../../store_kit_wrappers.dart'; + +part 'enum_converters.g.dart'; + +/// Serializer for [SKPaymentTransactionStateWrapper]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@SKTransactionStatusConverter()`. +class SKTransactionStatusConverter + implements JsonConverter { + /// Default const constructor. + const SKTransactionStatusConverter(); + + @override + SKPaymentTransactionStateWrapper fromJson(int? json) { + if (json == null) { + return SKPaymentTransactionStateWrapper.unspecified; + } + return _$enumDecode( + _$SKPaymentTransactionStateWrapperEnumMap + .cast(), + json); + } + + /// Converts an [SKPaymentTransactionStateWrapper] to a [PurchaseStatus]. + PurchaseStatus toPurchaseStatus(SKPaymentTransactionStateWrapper object) { + switch (object) { + case SKPaymentTransactionStateWrapper.purchasing: + case SKPaymentTransactionStateWrapper.deferred: + return PurchaseStatus.pending; + case SKPaymentTransactionStateWrapper.purchased: + case SKPaymentTransactionStateWrapper.restored: + return PurchaseStatus.purchased; + case SKPaymentTransactionStateWrapper.failed: + case SKPaymentTransactionStateWrapper.unspecified: + return PurchaseStatus.error; + } + } + + @override + int toJson(SKPaymentTransactionStateWrapper object) => + _$SKPaymentTransactionStateWrapperEnumMap[object]!; +} + +/// Serializer for [SKSubscriptionPeriodUnit]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@SKSubscriptionPeriodUnitConverter()`. +class SKSubscriptionPeriodUnitConverter + implements JsonConverter { + /// Default const constructor. + const SKSubscriptionPeriodUnitConverter(); + + @override + SKSubscriptionPeriodUnit fromJson(int? json) { + if (json == null) { + return SKSubscriptionPeriodUnit.day; + } + return _$enumDecode( + _$SKSubscriptionPeriodUnitEnumMap + .cast(), + json); + } + + @override + int toJson(SKSubscriptionPeriodUnit object) => + _$SKSubscriptionPeriodUnitEnumMap[object]!; +} + +/// Serializer for [SKProductDiscountPaymentMode]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@SKProductDiscountPaymentModeConverter()`. +class SKProductDiscountPaymentModeConverter + implements JsonConverter { + /// Default const constructor. + const SKProductDiscountPaymentModeConverter(); + + @override + SKProductDiscountPaymentMode fromJson(int? json) { + if (json == null) { + return SKProductDiscountPaymentMode.payAsYouGo; + } + return _$enumDecode( + _$SKProductDiscountPaymentModeEnumMap + .cast(), + json); + } + + @override + int toJson(SKProductDiscountPaymentMode object) => + _$SKProductDiscountPaymentModeEnumMap[object]!; +} + +// Define a class so we generate serializer helper methods for the enums +@JsonSerializable() +class _SerializedEnums { + late SKPaymentTransactionStateWrapper response; + late SKSubscriptionPeriodUnit unit; + late SKProductDiscountPaymentMode discountPaymentMode; +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.g.dart new file mode 100644 index 000000000000..b003f435a800 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.g.dart @@ -0,0 +1,73 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'enum_converters.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_SerializedEnums _$_SerializedEnumsFromJson(Map json) { + return _SerializedEnums() + ..response = _$enumDecode( + _$SKPaymentTransactionStateWrapperEnumMap, json['response']) + ..unit = _$enumDecode(_$SKSubscriptionPeriodUnitEnumMap, json['unit']) + ..discountPaymentMode = _$enumDecode( + _$SKProductDiscountPaymentModeEnumMap, json['discountPaymentMode']); +} + +Map _$_SerializedEnumsToJson(_SerializedEnums instance) => + { + 'response': _$SKPaymentTransactionStateWrapperEnumMap[instance.response], + 'unit': _$SKSubscriptionPeriodUnitEnumMap[instance.unit], + 'discountPaymentMode': + _$SKProductDiscountPaymentModeEnumMap[instance.discountPaymentMode], + }; + +K _$enumDecode( + Map enumValues, + Object? source, { + K? unknownValue, +}) { + if (source == null) { + throw ArgumentError( + 'A value must be provided. Supported values: ' + '${enumValues.values.join(', ')}', + ); + } + + return enumValues.entries.singleWhere( + (e) => e.value == source, + orElse: () { + if (unknownValue == null) { + throw ArgumentError( + '`$source` is not one of the supported values: ' + '${enumValues.values.join(', ')}', + ); + } + return MapEntry(unknownValue, enumValues.values.first); + }, + ).key; +} + +const _$SKPaymentTransactionStateWrapperEnumMap = { + SKPaymentTransactionStateWrapper.purchasing: 0, + SKPaymentTransactionStateWrapper.purchased: 1, + SKPaymentTransactionStateWrapper.failed: 2, + SKPaymentTransactionStateWrapper.restored: 3, + SKPaymentTransactionStateWrapper.deferred: 4, + SKPaymentTransactionStateWrapper.unspecified: -1, +}; + +const _$SKSubscriptionPeriodUnitEnumMap = { + SKSubscriptionPeriodUnit.day: 0, + SKSubscriptionPeriodUnit.week: 1, + SKSubscriptionPeriodUnit.month: 2, + SKSubscriptionPeriodUnit.year: 3, +}; + +const _$SKProductDiscountPaymentModeEnumMap = { + SKProductDiscountPaymentMode.payAsYouGo: 0, + SKProductDiscountPaymentMode.payUpFront: 1, + SKProductDiscountPaymentMode.freeTrail: 2, + SKProductDiscountPaymentMode.unspecified: -1, +}; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart new file mode 100644 index 000000000000..2ea6b5b2a6f8 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -0,0 +1,384 @@ +// 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 'dart:ui' show hashValues; + +import 'package:collection/collection.dart'; +import 'package:flutter/services.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; + +import '../channel.dart'; +import 'sk_payment_transaction_wrappers.dart'; +import 'sk_product_wrapper.dart'; + +part 'sk_payment_queue_wrapper.g.dart'; + +/// A wrapper around +/// [`SKPaymentQueue`](https://developer.apple.com/documentation/storekit/skpaymentqueue?language=objc). +/// +/// The payment queue contains payment related operations. It communicates with +/// the App Store and presents a user interface for the user to process and +/// authorize payments. +/// +/// Full information on using `SKPaymentQueue` and processing purchases is +/// available at the [In-App Purchase Programming +/// Guide](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Introduction.html#//apple_ref/doc/uid/TP40008267). +class SKPaymentQueueWrapper { + /// Returns the default payment queue. + /// + /// We do not support instantiating a custom payment queue, hence the + /// singleton. However, you can override the observer. + factory SKPaymentQueueWrapper() { + return _singleton; + } + + SKPaymentQueueWrapper._(); + + static final SKPaymentQueueWrapper _singleton = SKPaymentQueueWrapper._(); + + SKTransactionObserverWrapper? _observer; + + /// Calls [`-[SKPaymentQueue transactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506026-transactions?language=objc) + Future> transactions() async { + return _getTransactionList((await channel + .invokeListMethod('-[SKPaymentQueue transactions]'))!); + } + + /// Calls [`-[SKPaymentQueue canMakePayments:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506139-canmakepayments?language=objc). + static Future canMakePayments() async => + (await channel + .invokeMethod('-[SKPaymentQueue canMakePayments:]')) ?? + false; + + /// Sets an observer to listen to all incoming transaction events. + /// + /// This should be called and set as soon as the app launches in order to + /// avoid missing any purchase updates from the App Store. See the + /// documentation on StoreKit's [`-[SKPaymentQueue + /// addTransactionObserver:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506042-addtransactionobserver?language=objc). + void setTransactionObserver(SKTransactionObserverWrapper observer) { + _observer = observer; + channel.setMethodCallHandler(_handleObserverCallbacks); + } + + /// Posts a payment to the queue. + /// + /// This sends a purchase request to the App Store for confirmation. + /// Transaction updates will be delivered to the set + /// [SkTransactionObserverWrapper]. + /// + /// A couple preconditions need to be met before calling this method. + /// + /// - At least one [SKTransactionObserverWrapper] should have been added to + /// the payment queue using [addTransactionObserver]. + /// - The [payment.productIdentifier] needs to have been previously fetched + /// using [SKRequestMaker.startProductRequest] so that a valid `SKProduct` + /// has been cached in the platform side already. Because of this + /// [payment.productIdentifier] cannot be hardcoded. + /// + /// This method calls StoreKit's [`-[SKPaymentQueue addPayment:]`] + /// (https://developer.apple.com/documentation/storekit/skpaymentqueue/1506036-addpayment?preferredLanguage=occ). + /// + /// Also see [sandbox + /// testing](https://developer.apple.com/apple-pay/sandbox-testing/). + Future addPayment(SKPaymentWrapper payment) async { + assert(_observer != null, + '[in_app_purchase]: Trying to add a payment without an observer. One must be set using `SkPaymentQueueWrapper.setTransactionObserver` before the app launches.'); + final Map requestMap = payment.toMap(); + await channel.invokeMethod( + '-[InAppPurchasePlugin addPayment:result:]', + requestMap, + ); + } + + /// Finishes a transaction and removes it from the queue. + /// + /// This method should be called after the given [transaction] has been + /// succesfully processed and its content has been delivered to the user. + /// Transaction status updates are propagated to [SkTransactionObserver]. + /// + /// This will throw a Platform exception if [transaction.transactionState] is + /// [SKPaymentTransactionStateWrapper.purchasing]. + /// + /// This method calls StoreKit's [`-[SKPaymentQueue + /// finishTransaction:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506003-finishtransaction?language=objc). + Future finishTransaction( + SKPaymentTransactionWrapper transaction) async { + Map requestMap = transaction.toFinishMap(); + await channel.invokeMethod( + '-[InAppPurchasePlugin finishTransaction:result:]', + requestMap, + ); + } + + /// Restore previously purchased transactions. + /// + /// Use this to load previously purchased content on a new device. + /// + /// This call triggers purchase updates on the set + /// [SKTransactionObserverWrapper] for previously made transactions. This will + /// invoke [SKTransactionObserverWrapper.restoreCompletedTransactions], + /// [SKTransactionObserverWrapper.paymentQueueRestoreCompletedTransactionsFinished], + /// and [SKTransactionObserverWrapper.updatedTransaction]. These restored + /// transactions need to be marked complete with [finishTransaction] once the + /// content is delivered, like any other transaction. + /// + /// The `applicationUserName` should match the original + /// [SKPaymentWrapper.applicationUsername] used in [addPayment]. + /// If no `applicationUserName` was used, `applicationUserName` should be null. + /// + /// This method either triggers [`-[SKPayment + /// restoreCompletedTransactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506123-restorecompletedtransactions?language=objc) + /// or [`-[SKPayment restoreCompletedTransactionsWithApplicationUsername:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1505992-restorecompletedtransactionswith?language=objc) + /// depending on whether the `applicationUserName` is set. + Future restoreTransactions({String? applicationUserName}) async { + await channel.invokeMethod( + '-[InAppPurchasePlugin restoreTransactions:result:]', + applicationUserName); + } + + /// Present Code Redemption Sheet + /// + /// Use this to allow Users to enter and redeem Codes + /// + /// This method triggers [`-[SKPayment + /// presentCodeRedemptionSheet]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3566726-presentcoderedemptionsheet?language=objc) + Future presentCodeRedemptionSheet() async { + await channel.invokeMethod( + '-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]'); + } + + // Triage a method channel call from the platform and triggers the correct observer method. + Future _handleObserverCallbacks(MethodCall call) async { + assert(_observer != null, + '[in_app_purchase]: (Fatal)The observer has not been set but we received a purchase transaction notification. Please ensure the observer has been set using `setTransactionObserver`. Make sure the observer is added right at the App Launch.'); + final SKTransactionObserverWrapper observer = _observer!; + switch (call.method) { + case 'updatedTransactions': + { + final List transactions = + _getTransactionList(call.arguments); + return Future(() { + observer.updatedTransactions(transactions: transactions); + }); + } + case 'removedTransactions': + { + final List transactions = + _getTransactionList(call.arguments); + return Future(() { + observer.removedTransactions(transactions: transactions); + }); + } + case 'restoreCompletedTransactionsFailed': + { + SKError error = SKError.fromJson(call.arguments); + return Future(() { + observer.restoreCompletedTransactionsFailed(error: error); + }); + } + case 'paymentQueueRestoreCompletedTransactionsFinished': + { + return Future(() { + observer.paymentQueueRestoreCompletedTransactionsFinished(); + }); + } + case 'shouldAddStorePayment': + { + SKPaymentWrapper payment = + SKPaymentWrapper.fromJson(call.arguments['payment']); + SKProductWrapper product = + SKProductWrapper.fromJson(call.arguments['product']); + return Future(() { + if (observer.shouldAddStorePayment( + payment: payment, product: product) == + true) { + SKPaymentQueueWrapper().addPayment(payment); + } + }); + } + default: + break; + } + throw PlatformException( + code: 'no_such_callback', + message: 'Did not recognize the observer callback ${call.method}.'); + } + + // Get transaction wrapper object list from arguments. + List _getTransactionList( + List transactionsData) { + return transactionsData.map((dynamic map) { + return SKPaymentTransactionWrapper.fromJson( + Map.castFrom(map)); + }).toList(); + } +} + +/// Dart wrapper around StoreKit's +/// [NSError](https://developer.apple.com/documentation/foundation/nserror?language=objc). +@immutable +@JsonSerializable() +class SKError { + /// Creates a new [SKError] object with the provided information. + const SKError({required this.code, required this.domain, required this.userInfo}); + + /// Constructs an instance of this from a key-value map of data. + /// + /// The map needs to have named string keys with values matching the names and + /// types of all of the members on this class. The `map` parameter must not be + /// null. + factory SKError.fromJson(Map map) { + return _$SKErrorFromJson(map); + } + + /// Error [code](https://developer.apple.com/documentation/foundation/1448136-nserror_codes) + /// as defined in the Cocoa Framework. + @JsonKey(defaultValue: 0) + final int code; + + /// Error + /// [domain](https://developer.apple.com/documentation/foundation/nscocoaerrordomain?language=objc) + /// as defined in the Cocoa Framework. + @JsonKey(defaultValue: '') + final String domain; + + /// A map that contains more detailed information about the error. + /// + /// Any key of the map must be a valid [NSErrorUserInfoKey](https://developer.apple.com/documentation/foundation/nserroruserinfokey?language=objc). + @JsonKey(defaultValue: {}) + final Map userInfo; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + final SKError typedOther = other as SKError; + return typedOther.code == code && + typedOther.domain == domain && + DeepCollectionEquality.unordered() + .equals(typedOther.userInfo, userInfo); + } + + @override + int get hashCode => hashValues( + code, + domain, + userInfo, + ); +} + +/// Dart wrapper around StoreKit's +/// [SKPayment](https://developer.apple.com/documentation/storekit/skpayment?language=objc). +/// +/// Used as the parameter to initiate a payment. In general, a developer should +/// not need to create the payment object explicitly; instead, use +/// [SKPaymentQueueWrapper.addPayment] directly with a product identifier to +/// initiate a payment. +@immutable +@JsonSerializable() +class SKPaymentWrapper { + /// Creates a new [SKPaymentWrapper] with the provided information. + const SKPaymentWrapper( + {required this.productIdentifier, + this.applicationUsername, + this.requestData, + this.quantity = 1, + this.simulatesAskToBuyInSandbox = false}); + + /// Constructs an instance of this from a key value map of data. + /// + /// The map needs to have named string keys with values matching the names and + /// types of all of the members on this class. The `map` parameter must not be + /// null. + factory SKPaymentWrapper.fromJson(Map map) { + assert(map != null); + return _$SKPaymentWrapperFromJson(map); + } + + /// Creates a Map object describes the payment object. + Map toMap() { + return { + 'productIdentifier': productIdentifier, + 'applicationUsername': applicationUsername, + 'requestData': requestData, + 'quantity': quantity, + 'simulatesAskToBuyInSandbox': simulatesAskToBuyInSandbox + }; + } + + /// The id for the product that the payment is for. + @JsonKey(defaultValue: '') + final String productIdentifier; + + /// An opaque id for the user's account. + /// + /// Used to help the store detect irregular activity. See + /// [applicationUsername](https://developer.apple.com/documentation/storekit/skpayment/1506116-applicationusername?language=objc) + /// for more details. For example, you can use a one-way hash of the user’s + /// account name on your server. Don’t use the Apple ID for your developer + /// account, the user’s Apple ID, or the user’s plaintext account name on + /// your server. + final String? applicationUsername; + + /// Reserved for future use. + /// + /// The value must be null before sending the payment. If the value is not + /// null, the payment will be rejected. + /// + // The iOS Platform provided this property but it is reserved for future use. + // We also provide this property to match the iOS platform. Converted to + // String from NSData from ios platform using UTF8Encoding. The / default is + // null. + final String? requestData; + + /// The amount of the product this payment is for. + /// + /// The default is 1. The minimum is 1. The maximum is 10. + /// + /// If the object is invalid, the value could be 0. + @JsonKey(defaultValue: 0) + final int quantity; + + /// Produces an "ask to buy" flow in the sandbox. + /// + /// Setting it to `true` will cause a transaction to be in the state [SKPaymentTransactionStateWrapper.deferred], + /// which produce an "ask to buy" prompt that interrupts the the payment flow. + /// + /// Default is `false`. + /// + /// See https://developer.apple.com/in-app-purchase/ for a guide on Sandbox + /// testing. + @JsonKey(defaultValue: false) + final bool simulatesAskToBuyInSandbox; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + final SKPaymentWrapper typedOther = other as SKPaymentWrapper; + return typedOther.productIdentifier == productIdentifier && + typedOther.applicationUsername == applicationUsername && + typedOther.quantity == quantity && + typedOther.simulatesAskToBuyInSandbox == simulatesAskToBuyInSandbox && + typedOther.requestData == requestData; + } + + @override + int get hashCode => hashValues(productIdentifier, applicationUsername, + quantity, simulatesAskToBuyInSandbox, requestData); + + @override + String toString() => _$SKPaymentWrapperToJson(this).toString(); +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart new file mode 100644 index 000000000000..2b886597adc5 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart @@ -0,0 +1,44 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sk_payment_queue_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SKError _$SKErrorFromJson(Map json) { + return SKError( + code: json['code'] as int? ?? 0, + domain: json['domain'] as String? ?? '', + userInfo: (json['userInfo'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + ) ?? + {}, + ); +} + +Map _$SKErrorToJson(SKError instance) => { + 'code': instance.code, + 'domain': instance.domain, + 'userInfo': instance.userInfo, + }; + +SKPaymentWrapper _$SKPaymentWrapperFromJson(Map json) { + return SKPaymentWrapper( + productIdentifier: json['productIdentifier'] as String? ?? '', + applicationUsername: json['applicationUsername'] as String?, + requestData: json['requestData'] as String?, + quantity: json['quantity'] as int? ?? 0, + simulatesAskToBuyInSandbox: + json['simulatesAskToBuyInSandbox'] as bool? ?? false, + ); +} + +Map _$SKPaymentWrapperToJson(SKPaymentWrapper instance) => + { + 'productIdentifier': instance.productIdentifier, + 'applicationUsername': instance.applicationUsername, + 'requestData': instance.requestData, + 'quantity': instance.quantity, + 'simulatesAskToBuyInSandbox': instance.simulatesAskToBuyInSandbox, + }; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart new file mode 100644 index 000000000000..01cd6db0dda1 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart @@ -0,0 +1,203 @@ +// 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:ui' show hashValues; +import 'package:json_annotation/json_annotation.dart'; +import 'sk_product_wrapper.dart'; +import 'sk_payment_queue_wrapper.dart'; +import 'enum_converters.dart'; + +part 'sk_payment_transaction_wrappers.g.dart'; + +/// Callback handlers for transaction status changes. +/// +/// Must be subclassed. Must be instantiated and added to the +/// [SKPaymentQueueWrapper] via [SKPaymentQueueWrapper.setTransactionObserver] +/// at app launch. +/// +/// This class is a Dart wrapper around [SKTransactionObserver](https://developer.apple.com/documentation/storekit/skpaymenttransactionobserver?language=objc). +abstract class SKTransactionObserverWrapper { + /// Triggered when any transactions are updated. + void updatedTransactions( + {required List transactions}); + + /// Triggered when any transactions are removed from the payment queue. + void removedTransactions( + {required List transactions}); + + /// Triggered when there is an error while restoring transactions. + void restoreCompletedTransactionsFailed({required SKError error}); + + /// Triggered when payment queue has finished sending restored transactions. + void paymentQueueRestoreCompletedTransactionsFinished(); + + /// Triggered when a user initiates an in-app purchase from App Store. + /// + /// Return `true` to continue the transaction in your app. If you have + /// multiple [SKTransactionObserverWrapper]s, the transaction will continue if + /// any [SKTransactionObserverWrapper] returns `true`. Return `false` to defer + /// or cancel the transaction. For example, you may need to defer a + /// transaction if the user is in the middle of onboarding. You can also + /// continue the transaction later by calling [addPayment] with the + /// `payment` param from this method. + bool shouldAddStorePayment( + {required SKPaymentWrapper payment, required SKProductWrapper product}); +} + +/// The state of a transaction. +/// +/// Dart wrapper around StoreKit's +/// [SKPaymentTransactionState](https://developer.apple.com/documentation/storekit/skpaymenttransactionstate?language=objc). +enum SKPaymentTransactionStateWrapper { + /// Indicates the transaction is being processed in App Store. + /// + /// You should update your UI to indicate that you are waiting for the + /// transaction to update to another state. Never complete a transaction that + /// is still in a purchasing state. + @JsonValue(0) + purchasing, + + /// The user's payment has been succesfully processed. + /// + /// You should provide the user the content that they purchased. + @JsonValue(1) + purchased, + + /// The transaction failed. + /// + /// Check the [SKPaymentTransactionWrapper.error] property from + /// [SKPaymentTransactionWrapper] for details. + @JsonValue(2) + failed, + + /// This transaction is restoring content previously purchased by the user. + /// + /// The previous transaction information can be obtained in + /// [SKPaymentTransactionWrapper.originalTransaction] from + /// [SKPaymentTransactionWrapper]. + @JsonValue(3) + restored, + + /// The transaction is in the queue but pending external action. Wait for + /// another callback to get the final state. + /// + /// You should update your UI to indicate that you are waiting for the + /// transaction to update to another state. + @JsonValue(4) + deferred, + + /// Indicates the transaction is in an unspecified state. + @JsonValue(-1) + unspecified, +} + +/// Created when a payment is added to the [SKPaymentQueueWrapper]. +/// +/// Transactions are delivered to your app when a payment is finished +/// processing. Completed transactions provide a receipt and a transaction +/// identifier that the app can use to save a permanent record of the processed +/// payment. +/// +/// Dart wrapper around StoreKit's +/// [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction?language=objc). +@JsonSerializable() +class SKPaymentTransactionWrapper { + /// Creates a new [SKPaymentTransactionWrapper] with the provided information. + SKPaymentTransactionWrapper({ + required this.payment, + required this.transactionState, + this.originalTransaction, + this.transactionTimeStamp, + this.transactionIdentifier, + this.error, + }); + + /// Constructs an instance of this from a key value map of data. + /// + /// The map needs to have named string keys with values matching the names and + /// types of all of the members on this class. The `map` parameter must not be + /// null. + factory SKPaymentTransactionWrapper.fromJson(Map map) { + return _$SKPaymentTransactionWrapperFromJson(map); + } + + /// Current transaction state. + @SKTransactionStatusConverter() + final SKPaymentTransactionStateWrapper transactionState; + + /// The payment that has been created and added to the payment queue which + /// generated this transaction. + final SKPaymentWrapper payment; + + /// The original Transaction. + /// + /// Only available if the [transactionState] is [SKPaymentTransactionStateWrapper.restored]. + /// Otherwise the value is `null`. + /// + /// When the [transactionState] + /// is [SKPaymentTransactionStateWrapper.restored], the current transaction + /// object holds a new [transactionIdentifier]. + final SKPaymentTransactionWrapper? originalTransaction; + + /// The timestamp of the transaction. + /// + /// Seconds since epoch. It is only defined when the [transactionState] is + /// [SKPaymentTransactionStateWrapper.purchased] or + /// [SKPaymentTransactionStateWrapper.restored]. + /// Otherwise, the value is `null`. + final double? transactionTimeStamp; + + /// The unique string identifer of the transaction. + /// + /// It is only defined when the [transactionState] is + /// [SKPaymentTransactionStateWrapper.purchased] or + /// [SKPaymentTransactionStateWrapper.restored]. You may wish to record this + /// string as part of an audit trail for App Store purchases. The value of + /// this string corresponds to the same property in the receipt. + /// + /// The value is `null` if it is an unsuccessful transaction. + final String? transactionIdentifier; + + /// The error object + /// + /// Only available if the [transactionState] is + /// [SKPaymentTransactionStateWrapper.failed]. + final SKError? error; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + final SKPaymentTransactionWrapper typedOther = + other as SKPaymentTransactionWrapper; + return typedOther.payment == payment && + typedOther.transactionState == transactionState && + typedOther.originalTransaction == originalTransaction && + typedOther.transactionTimeStamp == transactionTimeStamp && + typedOther.transactionIdentifier == transactionIdentifier && + typedOther.error == error; + } + + @override + int get hashCode => hashValues( + this.payment, + this.transactionState, + this.originalTransaction, + this.transactionTimeStamp, + this.transactionIdentifier, + this.error); + + @override + String toString() => _$SKPaymentTransactionWrapperToJson(this).toString(); + + /// The payload that is used to finish this transaction. + Map toFinishMap() => { + "transactionIdentifier": this.transactionIdentifier, + "productIdentifier": this.payment.productIdentifier, + }; +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart new file mode 100644 index 000000000000..4c7af21bc151 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart @@ -0,0 +1,37 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sk_payment_transaction_wrappers.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SKPaymentTransactionWrapper _$SKPaymentTransactionWrapperFromJson(Map json) { + return SKPaymentTransactionWrapper( + payment: SKPaymentWrapper.fromJson( + Map.from(json['payment'] as Map)), + transactionState: const SKTransactionStatusConverter() + .fromJson(json['transactionState'] as int?), + originalTransaction: json['originalTransaction'] == null + ? null + : SKPaymentTransactionWrapper.fromJson( + Map.from(json['originalTransaction'] as Map)), + transactionTimeStamp: (json['transactionTimeStamp'] as num?)?.toDouble(), + transactionIdentifier: json['transactionIdentifier'] as String?, + error: json['error'] == null + ? null + : SKError.fromJson(Map.from(json['error'] as Map)), + ); +} + +Map _$SKPaymentTransactionWrapperToJson( + SKPaymentTransactionWrapper instance) => + { + 'transactionState': const SKTransactionStatusConverter() + .toJson(instance.transactionState), + 'payment': instance.payment, + 'originalTransaction': instance.originalTransaction, + 'transactionTimeStamp': instance.transactionTimeStamp, + 'transactionIdentifier': instance.transactionIdentifier, + 'error': instance.error, + }; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.dart new file mode 100644 index 000000000000..ef0e6671d177 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.dart @@ -0,0 +1,374 @@ +// 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:ui' show hashValues; +import 'package:collection/collection.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'enum_converters.dart'; + +// WARNING: Changes to `@JsonSerializable` classes need to be reflected in the +// below generated file. Run `flutter packages pub run build_runner watch` to +// rebuild and watch for further changes. +part 'sk_product_wrapper.g.dart'; + +/// Dart wrapper around StoreKit's [SKProductsResponse](https://developer.apple.com/documentation/storekit/skproductsresponse?language=objc). +/// +/// Represents the response object returned by [SKRequestMaker.startProductRequest]. +/// Contains information about a list of products and a list of invalid product identifiers. +@JsonSerializable() +class SkProductResponseWrapper { + /// Creates an [SkProductResponseWrapper] with the given product details. + SkProductResponseWrapper( + {required this.products, required this.invalidProductIdentifiers}); + + /// Constructing an instance from a map from the Objective-C layer. + /// + /// This method should only be used with `map` values returned by [SKRequestMaker.startProductRequest]. + factory SkProductResponseWrapper.fromJson(Map map) { + return _$SkProductResponseWrapperFromJson(map); + } + + /// Stores all matching successfully found products. + /// + /// One product in this list matches one valid product identifier passed to the [SKRequestMaker.startProductRequest]. + /// Will be empty if the [SKRequestMaker.startProductRequest] method does not pass any correct product identifier. + @JsonKey(defaultValue: []) + final List products; + + /// Stores product identifiers in the `productIdentifiers` from [SKRequestMaker.startProductRequest] that are not recognized by the App Store. + /// + /// The App Store will not recognize a product identifier unless certain criteria are met. A detailed list of the criteria can be + /// found here https://developer.apple.com/documentation/storekit/skproductsresponse/1505985-invalidproductidentifiers?language=objc. + /// Will be empty if all the product identifiers are valid. + @JsonKey(defaultValue: []) + final List invalidProductIdentifiers; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + final SkProductResponseWrapper typedOther = + other as SkProductResponseWrapper; + return DeepCollectionEquality().equals(typedOther.products, products) && + DeepCollectionEquality().equals( + typedOther.invalidProductIdentifiers, invalidProductIdentifiers); + } + + @override + int get hashCode => hashValues(this.products, this.invalidProductIdentifiers); +} + +/// Dart wrapper around StoreKit's [SKProductPeriodUnit](https://developer.apple.com/documentation/storekit/skproductperiodunit?language=objc). +/// +/// Used as a property in the [SKProductSubscriptionPeriodWrapper]. Minimum is a day and maximum is a year. +// The values of the enum options are matching the [SKProductPeriodUnit]'s values. Should there be an update or addition +// in the [SKProductPeriodUnit], this need to be updated to match. +enum SKSubscriptionPeriodUnit { + /// An interval lasting one day. + @JsonValue(0) + day, + + /// An interval lasting one month. + @JsonValue(1) + + /// An interval lasting one week. + week, + @JsonValue(2) + + /// An interval lasting one month. + month, + + /// An interval lasting one year. + @JsonValue(3) + year, +} + +/// Dart wrapper around StoreKit's [SKProductSubscriptionPeriod](https://developer.apple.com/documentation/storekit/skproductsubscriptionperiod?language=objc). +/// +/// A period is defined by a [numberOfUnits] and a [unit], e.g for a 3 months period [numberOfUnits] is 3 and [unit] is a month. +/// It is used as a property in [SKProductDiscountWrapper] and [SKProductWrapper]. +@JsonSerializable() +class SKProductSubscriptionPeriodWrapper { + /// Creates an [SKProductSubscriptionPeriodWrapper] for a `numberOfUnits`x`unit` period. + SKProductSubscriptionPeriodWrapper( + {required this.numberOfUnits, required this.unit}); + + /// Constructing an instance from a map from the Objective-C layer. + /// + /// This method should only be used with `map` values returned by [SKProductDiscountWrapper.fromJson] or [SKProductWrapper.fromJson]. + factory SKProductSubscriptionPeriodWrapper.fromJson( + Map? map) { + if (map == null) { + return SKProductSubscriptionPeriodWrapper( + numberOfUnits: 0, unit: SKSubscriptionPeriodUnit.day); + } + return _$SKProductSubscriptionPeriodWrapperFromJson(map); + } + + /// The number of [unit] units in this period. + /// + /// Must be greater than 0 if the object is valid. + @JsonKey(defaultValue: 0) + final int numberOfUnits; + + /// The time unit used to specify the length of this period. + @SKSubscriptionPeriodUnitConverter() + final SKSubscriptionPeriodUnit unit; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + final SKProductSubscriptionPeriodWrapper typedOther = + other as SKProductSubscriptionPeriodWrapper; + return typedOther.numberOfUnits == numberOfUnits && typedOther.unit == unit; + } + + @override + int get hashCode => hashValues(this.numberOfUnits, this.unit); +} + +/// Dart wrapper around StoreKit's [SKProductDiscountPaymentMode](https://developer.apple.com/documentation/storekit/skproductdiscountpaymentmode?language=objc). +/// +/// This is used as a property in the [SKProductDiscountWrapper]. +// The values of the enum options are matching the [SKProductDiscountPaymentMode]'s values. Should there be an update or addition +// in the [SKProductDiscountPaymentMode], this need to be updated to match. +enum SKProductDiscountPaymentMode { + /// Allows user to pay the discounted price at each payment period. + @JsonValue(0) + payAsYouGo, + + /// Allows user to pay the discounted price upfront and receive the product for the rest of time that was paid for. + @JsonValue(1) + payUpFront, + + /// User pays nothing during the discounted period. + @JsonValue(2) + freeTrail, + + /// Unspecified mode. + @JsonValue(-1) + unspecified, +} + +/// Dart wrapper around StoreKit's [SKProductDiscount](https://developer.apple.com/documentation/storekit/skproductdiscount?language=objc). +/// +/// It is used as a property in [SKProductWrapper]. +@JsonSerializable() +class SKProductDiscountWrapper { + /// Creates an [SKProductDiscountWrapper] with the given discount details. + SKProductDiscountWrapper( + {required this.price, + required this.priceLocale, + required this.numberOfPeriods, + required this.paymentMode, + required this.subscriptionPeriod}); + + /// Constructing an instance from a map from the Objective-C layer. + /// + /// This method should only be used with `map` values returned by [SKProductWrapper.fromJson]. + factory SKProductDiscountWrapper.fromJson(Map map) { + return _$SKProductDiscountWrapperFromJson(map); + } + + /// The discounted price, in the currency that is defined in [priceLocale]. + @JsonKey(defaultValue: '') + final String price; + + /// Includes locale information about the price, e.g. `$` as the currency symbol for US locale. + final SKPriceLocaleWrapper priceLocale; + + /// The object represent the discount period length. + /// + /// The value must be >= 0 if the object is valid. + @JsonKey(defaultValue: 0) + final int numberOfPeriods; + + /// The object indicates how the discount price is charged. + @SKProductDiscountPaymentModeConverter() + final SKProductDiscountPaymentMode paymentMode; + + /// The object represents the duration of single subscription period for the discount. + /// + /// The [subscriptionPeriod] of the discount is independent of the product's [subscriptionPeriod], + /// and their units and duration do not have to be matched. + final SKProductSubscriptionPeriodWrapper subscriptionPeriod; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + final SKProductDiscountWrapper typedOther = + other as SKProductDiscountWrapper; + return typedOther.price == price && + typedOther.priceLocale == priceLocale && + typedOther.numberOfPeriods == numberOfPeriods && + typedOther.paymentMode == paymentMode && + typedOther.subscriptionPeriod == subscriptionPeriod; + } + + @override + int get hashCode => hashValues(this.price, this.priceLocale, + this.numberOfPeriods, this.paymentMode, this.subscriptionPeriod); +} + +/// Dart wrapper around StoreKit's [SKProduct](https://developer.apple.com/documentation/storekit/skproduct?language=objc). +/// +/// A list of [SKProductWrapper] is returned in the [SKRequestMaker.startProductRequest] method, and +/// should be stored for use when making a payment. +@JsonSerializable() +class SKProductWrapper { + /// Creates an [SKProductWrapper] with the given product details. + SKProductWrapper({ + required this.productIdentifier, + required this.localizedTitle, + required this.localizedDescription, + required this.priceLocale, + this.subscriptionGroupIdentifier, + required this.price, + this.subscriptionPeriod, + this.introductoryPrice, + }); + + /// Constructing an instance from a map from the Objective-C layer. + /// + /// This method should only be used with `map` values returned by [SkProductResponseWrapper.fromJson]. + factory SKProductWrapper.fromJson(Map map) { + return _$SKProductWrapperFromJson(map); + } + + /// The unique identifier of the product. + @JsonKey(defaultValue: '') + final String productIdentifier; + + /// The localizedTitle of the product. + /// + /// It is localized based on the current locale. + @JsonKey(defaultValue: '') + final String localizedTitle; + + /// The localized description of the product. + /// + /// It is localized based on the current locale. + @JsonKey(defaultValue: '') + final String localizedDescription; + + /// Includes locale information about the price, e.g. `$` as the currency symbol for US locale. + final SKPriceLocaleWrapper priceLocale; + + /// The subscription group identifier. + /// + /// If the product is not a subscription, the value is `null`. + /// + /// A subscription group is a collection of subscription products. + /// Check [SubscriptionGroup](https://developer.apple.com/app-store/subscriptions/) for more details about subscription group. + final String? subscriptionGroupIdentifier; + + /// The price of the product, in the currency that is defined in [priceLocale]. + @JsonKey(defaultValue: '') + final String price; + + /// The object represents the subscription period of the product. + /// + /// Can be [null] is the product is not a subscription. + final SKProductSubscriptionPeriodWrapper? subscriptionPeriod; + + /// The object represents the duration of single subscription period. + /// + /// This is only available if you set up the introductory price in the App Store Connect, otherwise the value is `null`. + /// Programmer is also responsible to determine if the user is eligible to receive it. See https://developer.apple.com/documentation/storekit/in-app_purchase/offering_introductory_pricing_in_your_app?language=objc + /// for more details. + /// The [subscriptionPeriod] of the discount is independent of the product's [subscriptionPeriod], + /// and their units and duration do not have to be matched. + final SKProductDiscountWrapper? introductoryPrice; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + final SKProductWrapper typedOther = other as SKProductWrapper; + return typedOther.productIdentifier == productIdentifier && + typedOther.localizedTitle == localizedTitle && + typedOther.localizedDescription == localizedDescription && + typedOther.priceLocale == priceLocale && + typedOther.subscriptionGroupIdentifier == subscriptionGroupIdentifier && + typedOther.price == price && + typedOther.subscriptionPeriod == subscriptionPeriod && + typedOther.introductoryPrice == introductoryPrice; + } + + @override + int get hashCode => hashValues( + this.productIdentifier, + this.localizedTitle, + this.localizedDescription, + this.priceLocale, + this.subscriptionGroupIdentifier, + this.price, + this.subscriptionPeriod, + this.introductoryPrice); +} + +/// Object that indicates the locale of the price +/// +/// It is a thin wrapper of [NSLocale](https://developer.apple.com/documentation/foundation/nslocale?language=objc). +// TODO(cyanglaz): NSLocale is a complex object, want to see the actual need of getting this expanded. +// Matching android to only get the currencySymbol for now. +// https://github.com/flutter/flutter/issues/26610 +@JsonSerializable() +class SKPriceLocaleWrapper { + /// Creates a new price locale for `currencySymbol` and `currencyCode`. + SKPriceLocaleWrapper( + {required this.currencySymbol, required this.currencyCode}); + + /// Constructing an instance from a map from the Objective-C layer. + /// + /// This method should only be used with `map` values returned by [SKProductWrapper.fromJson] and [SKProductDiscountWrapper.fromJson]. + factory SKPriceLocaleWrapper.fromJson(Map? map) { + if (map == null) { + return SKPriceLocaleWrapper(currencyCode: '', currencySymbol: ''); + } + return _$SKPriceLocaleWrapperFromJson(map); + } + + ///The currency symbol for the locale, e.g. $ for US locale. + @JsonKey(defaultValue: '') + final String currencySymbol; + + ///The currency code for the locale, e.g. USD for US locale. + @JsonKey(defaultValue: '') + final String currencyCode; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + final SKPriceLocaleWrapper typedOther = other as SKPriceLocaleWrapper; + return typedOther.currencySymbol == currencySymbol && + typedOther.currencyCode == currencyCode; + } + + @override + int get hashCode => hashValues(this.currencySymbol, this.currencyCode); +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart new file mode 100644 index 000000000000..8c2eed3d6070 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart @@ -0,0 +1,123 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sk_product_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SkProductResponseWrapper _$SkProductResponseWrapperFromJson(Map json) { + return SkProductResponseWrapper( + products: (json['products'] as List?) + ?.map((e) => + SKProductWrapper.fromJson(Map.from(e as Map))) + .toList() ?? + [], + invalidProductIdentifiers: + (json['invalidProductIdentifiers'] as List?) + ?.map((e) => e as String) + .toList() ?? + [], + ); +} + +Map _$SkProductResponseWrapperToJson( + SkProductResponseWrapper instance) => + { + 'products': instance.products, + 'invalidProductIdentifiers': instance.invalidProductIdentifiers, + }; + +SKProductSubscriptionPeriodWrapper _$SKProductSubscriptionPeriodWrapperFromJson( + Map json) { + return SKProductSubscriptionPeriodWrapper( + numberOfUnits: json['numberOfUnits'] as int? ?? 0, + unit: const SKSubscriptionPeriodUnitConverter() + .fromJson(json['unit'] as int?), + ); +} + +Map _$SKProductSubscriptionPeriodWrapperToJson( + SKProductSubscriptionPeriodWrapper instance) => + { + 'numberOfUnits': instance.numberOfUnits, + 'unit': const SKSubscriptionPeriodUnitConverter().toJson(instance.unit), + }; + +SKProductDiscountWrapper _$SKProductDiscountWrapperFromJson(Map json) { + return SKProductDiscountWrapper( + price: json['price'] as String? ?? '', + priceLocale: + SKPriceLocaleWrapper.fromJson((json['priceLocale'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + numberOfPeriods: json['numberOfPeriods'] as int? ?? 0, + paymentMode: const SKProductDiscountPaymentModeConverter() + .fromJson(json['paymentMode'] as int?), + subscriptionPeriod: SKProductSubscriptionPeriodWrapper.fromJson( + (json['subscriptionPeriod'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + ); +} + +Map _$SKProductDiscountWrapperToJson( + SKProductDiscountWrapper instance) => + { + 'price': instance.price, + 'priceLocale': instance.priceLocale, + 'numberOfPeriods': instance.numberOfPeriods, + 'paymentMode': const SKProductDiscountPaymentModeConverter() + .toJson(instance.paymentMode), + 'subscriptionPeriod': instance.subscriptionPeriod, + }; + +SKProductWrapper _$SKProductWrapperFromJson(Map json) { + return SKProductWrapper( + productIdentifier: json['productIdentifier'] as String? ?? '', + localizedTitle: json['localizedTitle'] as String? ?? '', + localizedDescription: json['localizedDescription'] as String? ?? '', + priceLocale: + SKPriceLocaleWrapper.fromJson((json['priceLocale'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + subscriptionGroupIdentifier: json['subscriptionGroupIdentifier'] as String?, + price: json['price'] as String? ?? '', + subscriptionPeriod: json['subscriptionPeriod'] == null + ? null + : SKProductSubscriptionPeriodWrapper.fromJson( + (json['subscriptionPeriod'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + introductoryPrice: json['introductoryPrice'] == null + ? null + : SKProductDiscountWrapper.fromJson( + Map.from(json['introductoryPrice'] as Map)), + ); +} + +Map _$SKProductWrapperToJson(SKProductWrapper instance) => + { + 'productIdentifier': instance.productIdentifier, + 'localizedTitle': instance.localizedTitle, + 'localizedDescription': instance.localizedDescription, + 'priceLocale': instance.priceLocale, + 'subscriptionGroupIdentifier': instance.subscriptionGroupIdentifier, + 'price': instance.price, + 'subscriptionPeriod': instance.subscriptionPeriod, + 'introductoryPrice': instance.introductoryPrice, + }; + +SKPriceLocaleWrapper _$SKPriceLocaleWrapperFromJson(Map json) { + return SKPriceLocaleWrapper( + currencySymbol: json['currencySymbol'] as String? ?? '', + currencyCode: json['currencyCode'] as String? ?? '', + ); +} + +Map _$SKPriceLocaleWrapperToJson( + SKPriceLocaleWrapper instance) => + { + 'currencySymbol': instance.currencySymbol, + 'currencyCode': instance.currencyCode, + }; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_receipt_manager.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_receipt_manager.dart new file mode 100644 index 000000000000..3eb41cb66a14 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_receipt_manager.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. + +import 'dart:async'; + +import '../channel.dart'; + +///This class contains static methods to manage StoreKit receipts. +class SKReceiptManager { + /// Retrieve the receipt data from your application's main bundle. + /// + /// The receipt data will be based64 encoded. The structure of the payload is defined using ASN.1. + /// You can use the receipt data retrieved by this method to validate users' purchases. + /// There are 2 ways to do so. Either validate locally or validate with App Store. + /// For more details on how to validate the receipt data, 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). + /// If the receipt is invalid or missing, you can use [SKRequestMaker.startRefreshReceiptRequest] to request a new receipt. + static Future retrieveReceiptData() async { + return (await channel.invokeMethod( + '-[InAppPurchasePlugin retrieveReceiptData:result:]')) ?? + ''; + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_request_maker.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_request_maker.dart new file mode 100644 index 000000000000..d59f66fce2c9 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_request_maker.dart @@ -0,0 +1,59 @@ +// 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:flutter/services.dart'; + +import '../channel.dart'; +import 'sk_product_wrapper.dart'; + +/// A request maker that handles all the requests made by SKRequest subclasses. +/// +/// There are multiple [SKRequest](https://developer.apple.com/documentation/storekit/skrequest?language=objc) subclasses handling different requests in the `StoreKit` with multiple delegate methods, +/// we consolidated all the `SKRequest` subclasses into this class to make requests in a more straightforward way. +/// The request maker will create a SKRequest object, immediately starting it, and completing the future successfully or throw an exception depending on what happened to the request. +class SKRequestMaker { + /// Fetches product information for a list of given product identifiers. + /// + /// The `productIdentifiers` should contain legitimate product identifiers that you declared for the products in the iTunes Connect. Invalid identifiers + /// will be stored and returned in [SkProductResponseWrapper.invalidProductIdentifiers]. Duplicate values in `productIdentifiers` will be omitted. + /// If `productIdentifiers` is null, an `storekit_invalid_argument` error will be returned. If `productIdentifiers` is empty, a [SkProductResponseWrapper] + /// will still be returned with [SkProductResponseWrapper.products] being null. + /// + /// [SkProductResponseWrapper] is returned if there is no error during the request. + /// A [PlatformException] is thrown if the platform code making the request fails. + Future startProductRequest( + List productIdentifiers) async { + final Map? productResponseMap = + await channel.invokeMapMethod( + '-[InAppPurchasePlugin startProductRequest:result:]', + productIdentifiers, + ); + if (productResponseMap == null) { + throw PlatformException( + code: 'storekit_no_response', + message: 'StoreKit: Failed to get response from platform.', + ); + } + return SkProductResponseWrapper.fromJson(productResponseMap); + } + + /// Uses [SKReceiptRefreshRequest](https://developer.apple.com/documentation/storekit/skreceiptrefreshrequest?language=objc) to request a new receipt. + /// + /// If the receipt is invalid or missing, you can use this API to request a new receipt. + /// The [receiptProperties] is optional and it exists only for [sandbox testing](https://developer.apple.com/apple-pay/sandbox-testing/). In the production app, call this API without pass in the [receiptProperties] parameter. + /// To test in the sandbox, you can request a receipt with any combination of properties to test the state transitions related to [`Volume Purchase Plan`](https://www.apple.com/business/site/docs/VPP_Business_Guide.pdf) receipts. + /// The valid keys in the receiptProperties are below (All of them are of type bool): + /// * isExpired: whether the receipt is expired. + /// * isRevoked: whether the receipt has been revoked. + /// * isVolumePurchase: whether the receipt is a Volume Purchase Plan receipt. + Future startRefreshReceiptRequest( + {Map? receiptProperties}) { + return channel.invokeMethod( + '-[InAppPurchasePlugin refreshReceipt:result:]', + receiptProperties, + ); + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/store_kit_wrappers.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/store_kit_wrappers.dart new file mode 100644 index 000000000000..b687d238083c --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/store_kit_wrappers.dart @@ -0,0 +1,9 @@ +// 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/store_kit_wrappers/sk_payment_queue_wrapper.dart'; +export 'src/store_kit_wrappers/sk_payment_transaction_wrappers.dart'; +export 'src/store_kit_wrappers/sk_product_wrapper.dart'; +export 'src/store_kit_wrappers/sk_receipt_manager.dart'; +export 'src/store_kit_wrappers/sk_request_maker.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml new file mode 100644 index 000000000000..e951d945d49c --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -0,0 +1,33 @@ +name: in_app_purchase_ios +description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the iOS StoreKit Framework. +repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios +version: 1.0.0 + +# TODO(mvanbeusekom): Remove when in_app_purchase_platform_interface is published +publish_to: 'none' + +flutter: + plugin: + platforms: + ios: + pluginClass: InAppPurchasePlugin + +dependencies: + # TODO(mvanbeusekom): Replace with pub.dev version when in_app_purchase_platform_interface is published + in_app_purchase_platform_interface: + path: ../in_app_purchase_platform_interface + + flutter: + sdk: flutter + + meta: ^1.3.0 + +dev_dependencies: + build_runner: ^1.11.1 + json_serializable: ^4.1.1 + flutter_test: + sdk: flutter + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" \ 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 f8dc4c998494..cf459a9a9489 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 @@ -104,7 +104,9 @@ abstract class InAppPurchasePlatform extends PlatformInterface { /// * [restorePurchases], for restoring non consumable products. /// /// Calling this method for consumable items will cause unwanted behaviors! - Future buyNonConsumable({required PurchaseParam purchaseParam}) => + Future buyNonConsumable({ + required PurchaseParam purchaseParam, + }) => throw UnimplementedError('buyNonConsumable() has not been implemented.'); /// Buy a consumable product. @@ -169,7 +171,9 @@ abstract class InAppPurchasePlatform extends PlatformInterface { /// 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) => + Future completePurchase( + PurchaseDetails purchase, + ) => throw UnimplementedError('completePurchase() has not been implemented.'); /// Restore all previous purchases. 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 e1e563d6f905..983fb2df53c1 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,6 +3,8 @@ // found in the LICENSE file. /// The class represents the information of a product. +/// +/// class ProductDetails { /// Creates a new product details object with the provided details. ProductDetails({ @@ -10,6 +12,8 @@ class ProductDetails { required this.title, required this.description, required this.price, + required this.rawPrice, + required this.currencyCode, }); /// The identifier of the product. @@ -31,4 +35,13 @@ class ProductDetails { /// /// For example, on iOS it is specified in App Store Connect; on Android, it is specified in Google Play Console. final String price; + + /// The unformatted price of the product, specified in the App Store Connect or Sku in Google Play console based on the platform. + /// The currency unit for this value can be found in the [currencyCode] property. + /// The value always describes full units of the currency. (e.g. 2.45 in the case of $2.45) + final double rawPrice; + + /// The currency code for the price of the product. + /// Based on the price specified in the App Store Connect or Sku in Google Play console based on the platform. + final String currencyCode; } From 21eb28d9b65e0603ccd37503a10076535d025fdf Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 26 Apr 2021 22:59:06 +0200 Subject: [PATCH 20/33] Added StoreKit wrapper tests --- .../lib/in_app_purchase_ios.dart | 2 +- .../lib/src/in_app_purchase_ios_platform.dart | 1 - .../app_store_product_details.dart | 0 .../app_store_purchase_details.dart | 46 ++-- .../app_store_purchase_param.dart | 0 .../{models/models.dart => types/types.dart} | 0 .../in_app_purchase_ios/pubspec.yaml | 1 + .../sk_methodchannel_apis_test.dart | 231 ++++++++++++++++++ .../store_kit_wrappers/sk_product_test.dart | 185 ++++++++++++++ .../sk_test_stub_objects.dart | 147 +++++++++++ .../lib/src/types/purchase_details.dart | 3 +- 11 files changed, 598 insertions(+), 18 deletions(-) rename packages/in_app_purchase/in_app_purchase_ios/lib/src/{models => types}/app_store_product_details.dart (100%) rename packages/in_app_purchase/in_app_purchase_ios/lib/src/{models => types}/app_store_purchase_details.dart (65%) rename packages/in_app_purchase/in_app_purchase_ios/lib/src/{models => types}/app_store_purchase_param.dart (100%) rename packages/in_app_purchase/in_app_purchase_ios/lib/src/{models/models.dart => types/types.dart} (100%) create mode 100644 packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_test_stub_objects.dart diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/in_app_purchase_ios.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/in_app_purchase_ios.dart index 1eebc1dcb926..0cd8c22ba094 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/in_app_purchase_ios.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/in_app_purchase_ios.dart @@ -3,4 +3,4 @@ // found in the LICENSE file. export 'src/in_app_purchase_ios_platform.dart'; -export 'src/models/models.dart'; +export 'src/types/types.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart index 709471334006..077e64fe1373 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart @@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; -import '../in_app_purchase_ios.dart'; import '../in_app_purchase_ios.dart'; import '../store_kit_wrappers.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/models/app_store_product_details.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_product_details.dart similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/lib/src/models/app_store_product_details.dart rename to packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_product_details.dart diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/models/app_store_purchase_details.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.dart similarity index 65% rename from packages/in_app_purchase/in_app_purchase_ios/lib/src/models/app_store_purchase_details.dart rename to packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.dart index 388f7ca07669..d5a03ed2f8d3 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/models/app_store_purchase_details.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.dart @@ -13,23 +13,40 @@ import '../store_kit_wrappers/enum_converters.dart'; class AppStorePurchaseDetails extends PurchaseDetails { /// Creates a new AppStore specific purchase details object with the provided /// details. - AppStorePurchaseDetails({ - String? purchaseID, - required String productID, - required PurchaseVerificationData verificationData, - required String? transactionDate, - required this.skPaymentTransaction, - }) : super( - productID: productID, - purchaseID: purchaseID, - transactionDate: transactionDate, - verificationData: verificationData, - ); + AppStorePurchaseDetails( + {String? purchaseID, + required String productID, + required PurchaseVerificationData verificationData, + required String? transactionDate, + required this.skPaymentTransaction, + required PurchaseStatus status}) + : super( + productID: productID, + purchaseID: purchaseID, + transactionDate: transactionDate, + verificationData: verificationData, + status: status) { + this.status = status; + } /// Points back to the [SKPaymentTransactionWrapper] which was used to /// generate this [AppStorePurchaseDetails] object. final SKPaymentTransactionWrapper skPaymentTransaction; + late PurchaseStatus _status; + + /// The status that this [PurchaseDetails] is currently on. + PurchaseStatus get status => _status; + set status(PurchaseStatus status) { + if (status == PurchaseStatus.purchased || status == PurchaseStatus.error) { + _pendingCompletePurchase = true; + } + _status = status; + } + + bool _pendingCompletePurchase = false; + bool get pendingCompletePurchase => _pendingCompletePurchase; + /// Generate a [AppStorePurchaseDetails] object based on an iOS /// [SKPaymentTransactionWrapper] object. factory AppStorePurchaseDetails.fromSKTransaction( @@ -40,6 +57,8 @@ class AppStorePurchaseDetails extends PurchaseDetails { productID: transaction.payment.productIdentifier, purchaseID: transaction.transactionIdentifier, skPaymentTransaction: transaction, + status: SKTransactionStatusConverter() + .toPurchaseStatus(transaction.transactionState), transactionDate: transaction.transactionTimeStamp != null ? (transaction.transactionTimeStamp! * 1000).toInt().toString() : null, @@ -49,9 +68,6 @@ class AppStorePurchaseDetails extends PurchaseDetails { source: kIAPSource), ); - purchaseDetails.status = SKTransactionStatusConverter() - .toPurchaseStatus(transaction.transactionState); - if (purchaseDetails.status == PurchaseStatus.error) { purchaseDetails.error = IAPError( source: kIAPSource, diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/models/app_store_purchase_param.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_param.dart similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/lib/src/models/app_store_purchase_param.dart rename to packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_param.dart diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/models/models.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/types.dart similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/lib/src/models/models.dart rename to packages/in_app_purchase/in_app_purchase_ios/lib/src/types/types.dart diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml index e951d945d49c..034567bd1ef6 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: sdk: flutter meta: ^1.3.0 + test: ^1.16.0 dev_dependencies: build_runner: ^1.11.1 diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart new file mode 100644 index 000000000000..98f6dac9598f --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart @@ -0,0 +1,231 @@ +// 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/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_ios/src/channel.dart'; +import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; +import 'sk_test_stub_objects.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final FakeIOSPlatform fakeIOSPlatform = FakeIOSPlatform(); + + setUpAll(() { + SystemChannels.platform + .setMockMethodCallHandler(fakeIOSPlatform.onMethodCall); + }); + + setUp(() {}); + + tearDown(() { + fakeIOSPlatform.testReturnNull = false; + }); + + group('sk_request_maker', () { + test('get products method channel', () async { + SkProductResponseWrapper productResponseWrapper = + await SKRequestMaker().startProductRequest(['xxx']); + expect( + productResponseWrapper.products, + isNotEmpty, + ); + expect( + productResponseWrapper.products.first.priceLocale.currencySymbol, + '\$', + ); + + expect( + productResponseWrapper.products.first.priceLocale.currencySymbol, + isNot('A'), + ); + expect( + productResponseWrapper.products.first.priceLocale.currencyCode, + 'USD', + ); + expect( + productResponseWrapper.invalidProductIdentifiers, + isNotEmpty, + ); + + expect( + fakeIOSPlatform.startProductRequestParam, + ['xxx'], + ); + }); + + test('get products method channel should throw exception', () async { + fakeIOSPlatform.getProductRequestFailTest = true; + expect( + SKRequestMaker().startProductRequest(['xxx']), + throwsException, + ); + fakeIOSPlatform.getProductRequestFailTest = false; + }); + + test('refreshed receipt', () async { + int receiptCountBefore = fakeIOSPlatform.refreshReceipt; + await SKRequestMaker().startRefreshReceiptRequest( + receiptProperties: {"isExpired": true}); + expect(fakeIOSPlatform.refreshReceipt, receiptCountBefore + 1); + expect(fakeIOSPlatform.refreshReceiptParam, + {"isExpired": true}); + }); + }); + + group('sk_receipt_manager', () { + test('should get receipt (faking it by returning a `receipt data` string)', + () async { + String receiptData = await SKReceiptManager.retrieveReceiptData(); + expect(receiptData, 'receipt data'); + }); + }); + + group('sk_payment_queue', () { + test('canMakePayment should return true', () async { + expect(await SKPaymentQueueWrapper.canMakePayments(), true); + }); + + test('canMakePayment returns false if method channel returns null', + () async { + fakeIOSPlatform.testReturnNull = true; + expect(await SKPaymentQueueWrapper.canMakePayments(), false); + }); + + test('transactions should return a valid list of transactions', () async { + expect(await SKPaymentQueueWrapper().transactions(), isNotEmpty); + }); + + test( + 'throws if observer is not set for payment queue before adding payment', + () async { + expect(SKPaymentQueueWrapper().addPayment(dummyPayment), + throwsAssertionError); + }); + + test('should add payment to the payment queue', () async { + SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); + TestPaymentTransactionObserver observer = + TestPaymentTransactionObserver(); + queue.setTransactionObserver(observer); + await queue.addPayment(dummyPayment); + expect(fakeIOSPlatform.payments.first, equals(dummyPayment)); + }); + + test('should finish transaction', () async { + SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); + TestPaymentTransactionObserver observer = + TestPaymentTransactionObserver(); + queue.setTransactionObserver(observer); + await queue.finishTransaction(dummyTransaction); + expect(fakeIOSPlatform.transactionsFinished.first, + equals(dummyTransaction.toFinishMap())); + }); + + test('should restore transaction', () async { + SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); + TestPaymentTransactionObserver observer = + TestPaymentTransactionObserver(); + queue.setTransactionObserver(observer); + await queue.restoreTransactions(applicationUserName: 'aUserID'); + expect(fakeIOSPlatform.applicationNameHasTransactionRestored, 'aUserID'); + }); + }); + + group('Code Redemption Sheet', () { + test('presentCodeRedemptionSheet should not throw', () async { + expect(fakeIOSPlatform.presentCodeRedemption, false); + await SKPaymentQueueWrapper().presentCodeRedemptionSheet(); + expect(fakeIOSPlatform.presentCodeRedemption, true); + fakeIOSPlatform.presentCodeRedemption = false; + }); + }); +} + +class FakeIOSPlatform { + FakeIOSPlatform() { + channel.setMockMethodCallHandler(onMethodCall); + } + // get product request + List startProductRequestParam = []; + bool getProductRequestFailTest = false; + bool testReturnNull = false; + + // refresh receipt request + int refreshReceipt = 0; + late Map refreshReceiptParam; + + // payment queue + List payments = []; + List> transactionsFinished = []; + String applicationNameHasTransactionRestored = ''; + + // present Code Redemption + bool presentCodeRedemption = false; + + Future onMethodCall(MethodCall call) { + switch (call.method) { + // request makers + case '-[InAppPurchasePlugin startProductRequest:result:]': + List productIDS = + List.castFrom(call.arguments); + assert(productIDS is List, 'invalid argument type'); + startProductRequestParam = call.arguments; + if (getProductRequestFailTest) { + return Future>.value(null); + } + return Future>.value( + buildProductResponseMap(dummyProductResponseWrapper)); + case '-[InAppPurchasePlugin refreshReceipt:result:]': + refreshReceipt++; + refreshReceiptParam = + Map.castFrom(call.arguments); + return Future.sync(() {}); + // receipt manager + case '-[InAppPurchasePlugin retrieveReceiptData:result:]': + return Future.value('receipt data'); + // payment queue + case '-[SKPaymentQueue canMakePayments:]': + if (testReturnNull) { + return Future.value(null); + } + return Future.value(true); + case '-[SKPaymentQueue transactions]': + return Future>.value( + [buildTransactionMap(dummyTransaction)]); + case '-[InAppPurchasePlugin addPayment:result:]': + payments.add(SKPaymentWrapper.fromJson( + Map.from(call.arguments))); + return Future.sync(() {}); + case '-[InAppPurchasePlugin finishTransaction:result:]': + transactionsFinished.add(Map.from(call.arguments)); + return Future.sync(() {}); + case '-[InAppPurchasePlugin restoreTransactions:result:]': + applicationNameHasTransactionRestored = call.arguments; + return Future.sync(() {}); + case '-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]': + presentCodeRedemption = true; + return Future.sync(() {}); + } + return Future.sync(() {}); + } +} + +class TestPaymentTransactionObserver extends SKTransactionObserverWrapper { + void updatedTransactions( + {required List transactions}) {} + + void removedTransactions( + {required List transactions}) {} + + void restoreCompletedTransactionsFailed({required SKError error}) {} + + void paymentQueueRestoreCompletedTransactionsFinished() {} + + bool shouldAddStorePayment( + {required SKPaymentWrapper payment, required SKProductWrapper product}) { + return true; + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart new file mode 100644 index 000000000000..e707674b1935 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart @@ -0,0 +1,185 @@ +// 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_ios/src/types/app_store_product_details.dart'; +import 'package:in_app_purchase_ios/src/types/app_store_purchase_details.dart'; +import 'package:in_app_purchase_ios/src/store_kit_wrappers/sk_product_wrapper.dart'; +import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; +import 'package:test/test.dart'; + +import 'sk_test_stub_objects.dart'; + +void main() { + group('product related object wrapper test', () { + test( + 'SKProductSubscriptionPeriodWrapper should have property values consistent with map', + () { + final SKProductSubscriptionPeriodWrapper wrapper = + SKProductSubscriptionPeriodWrapper.fromJson( + buildSubscriptionPeriodMap(dummySubscription)!); + expect(wrapper, equals(dummySubscription)); + }); + + test( + 'SKProductSubscriptionPeriodWrapper should have properties to be default values if map is empty', + () { + final SKProductSubscriptionPeriodWrapper wrapper = + SKProductSubscriptionPeriodWrapper.fromJson({}); + expect(wrapper.numberOfUnits, 0); + expect(wrapper.unit, SKSubscriptionPeriodUnit.day); + }); + + test( + 'SKProductDiscountWrapper should have property values consistent with map', + () { + final SKProductDiscountWrapper wrapper = + SKProductDiscountWrapper.fromJson(buildDiscountMap(dummyDiscount)); + expect(wrapper, equals(dummyDiscount)); + }); + + test( + 'SKProductDiscountWrapper should have properties to be default if map is empty', + () { + final SKProductDiscountWrapper wrapper = + SKProductDiscountWrapper.fromJson({}); + expect(wrapper.price, ''); + expect(wrapper.priceLocale, + SKPriceLocaleWrapper(currencyCode: '', currencySymbol: '')); + expect(wrapper.numberOfPeriods, 0); + expect(wrapper.paymentMode, SKProductDiscountPaymentMode.payAsYouGo); + expect( + wrapper.subscriptionPeriod, + SKProductSubscriptionPeriodWrapper( + numberOfUnits: 0, unit: SKSubscriptionPeriodUnit.day)); + }); + + test('SKProductWrapper should have property values consistent with map', + () { + final SKProductWrapper wrapper = + SKProductWrapper.fromJson(buildProductMap(dummyProductWrapper)); + expect(wrapper, equals(dummyProductWrapper)); + }); + + test( + 'SKProductWrapper should have properties to be default if map is empty', + () { + final SKProductWrapper wrapper = + SKProductWrapper.fromJson({}); + expect(wrapper.productIdentifier, ''); + expect(wrapper.localizedTitle, ''); + expect(wrapper.localizedDescription, ''); + expect(wrapper.priceLocale, + SKPriceLocaleWrapper(currencyCode: '', currencySymbol: '')); + expect(wrapper.subscriptionGroupIdentifier, null); + expect(wrapper.price, ''); + expect(wrapper.subscriptionPeriod, null); + }); + + test('toProductDetails() should return correct Product object', () { + final SKProductWrapper wrapper = + SKProductWrapper.fromJson(buildProductMap(dummyProductWrapper)); + final AppStoreProductDetails product = AppStoreProductDetails.fromSKProduct(wrapper); + expect(product.title, wrapper.localizedTitle); + expect(product.description, wrapper.localizedDescription); + expect(product.id, wrapper.productIdentifier); + expect(product.price, + wrapper.priceLocale.currencySymbol + wrapper.price.toString()); + expect(product.skProduct, wrapper); + }); + + test('SKProductResponse wrapper should match', () { + final SkProductResponseWrapper wrapper = + SkProductResponseWrapper.fromJson( + buildProductResponseMap(dummyProductResponseWrapper)); + expect(wrapper, equals(dummyProductResponseWrapper)); + }); + test('SKProductResponse wrapper should default to empty list', () { + final Map> productResponseMapEmptyList = + >{ + 'products': >[], + 'invalidProductIdentifiers': [], + }; + final SkProductResponseWrapper wrapper = + SkProductResponseWrapper.fromJson(productResponseMapEmptyList); + expect(wrapper.products.length, 0); + expect(wrapper.invalidProductIdentifiers.length, 0); + }); + + test('LocaleWrapper should have property values consistent with map', () { + final SKPriceLocaleWrapper wrapper = + SKPriceLocaleWrapper.fromJson(buildLocaleMap(dummyLocale)); + expect(wrapper, equals(dummyLocale)); + }); + }); + + group('Payment queue related object tests', () { + test('Should construct correct SKPaymentWrapper from json', () { + SKPaymentWrapper payment = + SKPaymentWrapper.fromJson(dummyPayment.toMap()); + expect(payment, equals(dummyPayment)); + }); + + test('Should construct correct SKError from json', () { + SKError error = SKError.fromJson(buildErrorMap(dummyError)); + expect(error, equals(dummyError)); + }); + + test('Should construct correct SKTransactionWrapper from json', () { + SKPaymentTransactionWrapper transaction = + SKPaymentTransactionWrapper.fromJson( + buildTransactionMap(dummyTransaction)); + expect(transaction, equals(dummyTransaction)); + }); + + test('toPurchaseDetails() should return correct PurchaseDetail object', () { + AppStorePurchaseDetails details = + AppStorePurchaseDetails.fromSKTransaction(dummyTransaction, 'receipt data'); + expect(dummyTransaction.transactionIdentifier, details.purchaseID); + expect(dummyTransaction.payment.productIdentifier, details.productID); + expect(dummyTransaction.transactionTimeStamp, isNotNull); + expect((dummyTransaction.transactionTimeStamp! * 1000).toInt().toString(), + details.transactionDate); + expect(details.verificationData.localVerificationData, 'receipt data'); + expect(details.verificationData.serverVerificationData, 'receipt data'); + expect(details.verificationData.source, 'app_store'); + expect(details.skPaymentTransaction, dummyTransaction); + expect(details.pendingCompletePurchase, true); + }); + + test('SKPaymentTransactionWrapper.toFinishMap set correct value', () { + final SKPaymentTransactionWrapper transactionWrapper = + SKPaymentTransactionWrapper( + payment: dummyPayment, + transactionState: SKPaymentTransactionStateWrapper.failed, + transactionIdentifier: 'abcd'); + final Map finishMap = transactionWrapper.toFinishMap(); + expect(finishMap['transactionIdentifier'], 'abcd'); + expect(finishMap['productIdentifier'], dummyPayment.productIdentifier); + }); + + test( + 'SKPaymentTransactionWrapper.toFinishMap should set transactionIdentifier to null when necessary', + () { + final SKPaymentTransactionWrapper transactionWrapper = + SKPaymentTransactionWrapper( + payment: dummyPayment, + transactionState: SKPaymentTransactionStateWrapper.failed); + final Map finishMap = transactionWrapper.toFinishMap(); + expect(finishMap['transactionIdentifier'], null); + }); + + test('Should generate correct map of the payment object', () { + Map map = dummyPayment.toMap(); + expect(map['productIdentifier'], dummyPayment.productIdentifier); + expect(map['applicationUsername'], dummyPayment.applicationUsername); + + expect(map['requestData'], dummyPayment.requestData); + + expect(map['quantity'], dummyPayment.quantity); + + expect(map['simulatesAskToBuyInSandbox'], + dummyPayment.simulatesAskToBuyInSandbox); + }); + }); +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_test_stub_objects.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_test_stub_objects.dart new file mode 100644 index 000000000000..d6c24460761e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_test_stub_objects.dart @@ -0,0 +1,147 @@ +// 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_ios/store_kit_wrappers.dart'; + +final dummyPayment = SKPaymentWrapper( + productIdentifier: 'prod-id', + applicationUsername: 'app-user-name', + requestData: 'fake-data-utf8', + quantity: 2, + simulatesAskToBuyInSandbox: true); +final SKError dummyError = + SKError(code: 111, domain: 'dummy-domain', userInfo: {'key': 'value'}); + +final SKPaymentTransactionWrapper dummyOriginalTransaction = + SKPaymentTransactionWrapper( + transactionState: SKPaymentTransactionStateWrapper.purchased, + payment: dummyPayment, + originalTransaction: null, + transactionTimeStamp: 1231231231.00, + transactionIdentifier: '123123', + error: dummyError, +); + +final SKPaymentTransactionWrapper dummyTransaction = + SKPaymentTransactionWrapper( + transactionState: SKPaymentTransactionStateWrapper.purchased, + payment: dummyPayment, + originalTransaction: dummyOriginalTransaction, + transactionTimeStamp: 1231231231.00, + transactionIdentifier: '123123', + error: dummyError, +); + +final SKPriceLocaleWrapper dummyLocale = + SKPriceLocaleWrapper(currencySymbol: '\$', currencyCode: 'USD'); + +final SKProductSubscriptionPeriodWrapper dummySubscription = + SKProductSubscriptionPeriodWrapper( + numberOfUnits: 1, + unit: SKSubscriptionPeriodUnit.month, +); + +final SKProductDiscountWrapper dummyDiscount = SKProductDiscountWrapper( + price: '1.0', + priceLocale: dummyLocale, + numberOfPeriods: 1, + paymentMode: SKProductDiscountPaymentMode.payUpFront, + subscriptionPeriod: dummySubscription, +); + +final SKProductWrapper dummyProductWrapper = SKProductWrapper( + productIdentifier: 'id', + localizedTitle: 'title', + localizedDescription: 'description', + priceLocale: dummyLocale, + subscriptionGroupIdentifier: 'com.group', + price: '1.0', + subscriptionPeriod: dummySubscription, + introductoryPrice: dummyDiscount, +); + +final SkProductResponseWrapper dummyProductResponseWrapper = + SkProductResponseWrapper( + products: [dummyProductWrapper], + invalidProductIdentifiers: ['123'], +); + +Map buildLocaleMap(SKPriceLocaleWrapper local) { + return { + 'currencySymbol': local.currencySymbol, + 'currencyCode': local.currencyCode + }; +} + +Map? buildSubscriptionPeriodMap( + SKProductSubscriptionPeriodWrapper? sub) { + if (sub == null) { + return null; + } + return { + 'numberOfUnits': sub.numberOfUnits, + 'unit': SKSubscriptionPeriodUnit.values.indexOf(sub.unit), + }; +} + +Map buildDiscountMap(SKProductDiscountWrapper discount) { + return { + 'price': discount.price, + 'priceLocale': buildLocaleMap(discount.priceLocale), + 'numberOfPeriods': discount.numberOfPeriods, + 'paymentMode': + SKProductDiscountPaymentMode.values.indexOf(discount.paymentMode), + 'subscriptionPeriod': + buildSubscriptionPeriodMap(discount.subscriptionPeriod), + }; +} + +Map buildProductMap(SKProductWrapper product) { + return { + 'productIdentifier': product.productIdentifier, + 'localizedTitle': product.localizedTitle, + 'localizedDescription': product.localizedDescription, + 'priceLocale': buildLocaleMap(product.priceLocale), + 'subscriptionGroupIdentifier': product.subscriptionGroupIdentifier, + 'price': product.price, + 'subscriptionPeriod': + buildSubscriptionPeriodMap(product.subscriptionPeriod), + 'introductoryPrice': buildDiscountMap(product.introductoryPrice!), + }; +} + +Map buildProductResponseMap( + SkProductResponseWrapper response) { + List productsMap = response.products + .map((SKProductWrapper product) => buildProductMap(product)) + .toList(); + return { + 'products': productsMap, + 'invalidProductIdentifiers': response.invalidProductIdentifiers + }; +} + +Map buildErrorMap(SKError error) { + return { + 'code': error.code, + 'domain': error.domain, + 'userInfo': error.userInfo, + }; +} + +Map buildTransactionMap( + SKPaymentTransactionWrapper transaction) { + Map map = { + 'transactionState': SKPaymentTransactionStateWrapper.values + .indexOf(SKPaymentTransactionStateWrapper.purchased), + 'payment': transaction.payment.toMap(), + 'originalTransaction': transaction.originalTransaction == null + ? null + : buildTransactionMap(transaction.originalTransaction!), + 'transactionTimeStamp': transaction.transactionTimeStamp, + 'transactionIdentifier': transaction.transactionIdentifier, + 'error': buildErrorMap(transaction.error!), + }; + return map; +} 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 2638e306aa97..08d0efe09878 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 @@ -14,6 +14,7 @@ class PurchaseDetails { required this.productID, required this.verificationData, required this.transactionDate, + required this.status, }); /// A unique identifier of the purchase. @@ -37,7 +38,7 @@ class PurchaseDetails { final String? transactionDate; /// The status that this [PurchaseDetails] is currently on. - PurchaseStatus? status; + PurchaseStatus status; /// The error details when the [status] is [PurchaseStatus.error]. /// From 0cfc84d1e1e8c4880197530743bc511b6490270a Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 28 Apr 2021 11:44:45 +0200 Subject: [PATCH 21/33] Added unit-tests --- .../lib/src/in_app_purchase_ios_platform.dart | 20 +- .../store_kit_wrappers/enum_converters.dart | 3 +- .../sk_payment_queue_wrapper.dart | 3 +- .../src/types/app_store_purchase_details.dart | 4 +- .../lib/src/types/types.dart | 2 +- .../in_app_purchase_ios_platform_test.dart | 504 ++++++++++++++++++ .../store_kit_wrappers/sk_product_test.dart | 6 +- .../lib/src/types/product_details.dart | 4 +- 8 files changed, 527 insertions(+), 19 deletions(-) create mode 100644 packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart index 077e64fe1373..b9534c6d18c0 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart @@ -179,13 +179,15 @@ class InAppPurchaseIosPlatform implements InAppPurchasePlatform { class _TransactionObserver implements SKTransactionObserverWrapper { final StreamController> purchaseUpdatedController; - Completer>? _restoreCompleter; + Completer? _restoreCompleter; late String _receiptData; _TransactionObserver(this.purchaseUpdatedController); - Future restoreTransactions( - {required SKPaymentQueueWrapper queue, String? applicationUserName}) { + Future restoreTransactions({ + required SKPaymentQueueWrapper queue, + String? applicationUserName, + }) { _restoreCompleter = Completer(); queue.restoreTransactions(applicationUserName: applicationUserName); return _restoreCompleter!.future; @@ -198,12 +200,12 @@ class _TransactionObserver implements SKTransactionObserverWrapper { void updatedTransactions( {required List transactions}) async { String receiptData = await getReceiptData(); - purchaseUpdatedController - .add(transactions.map((SKPaymentTransactionWrapper transaction) { - AppStorePurchaseDetails purchaseDetails = - AppStorePurchaseDetails.fromSKTransaction(transaction, receiptData); - return purchaseDetails; - }).toList()); + List purchases = transactions + .map((SKPaymentTransactionWrapper transaction) => + AppStorePurchaseDetails.fromSKTransaction(transaction, receiptData)) + .toList(); + + purchaseUpdatedController.add(purchases); } void removedTransactions( diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart index a7235f6252fe..08af2c6058c4 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart @@ -36,8 +36,9 @@ class SKTransactionStatusConverter case SKPaymentTransactionStateWrapper.deferred: return PurchaseStatus.pending; case SKPaymentTransactionStateWrapper.purchased: - case SKPaymentTransactionStateWrapper.restored: return PurchaseStatus.purchased; + case SKPaymentTransactionStateWrapper.restored: + return PurchaseStatus.restored; case SKPaymentTransactionStateWrapper.failed: case SKPaymentTransactionStateWrapper.unspecified: return PurchaseStatus.error; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 2ea6b5b2a6f8..b677772869f6 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -224,7 +224,8 @@ class SKPaymentQueueWrapper { @JsonSerializable() class SKError { /// Creates a new [SKError] object with the provided information. - const SKError({required this.code, required this.domain, required this.userInfo}); + const SKError( + {required this.code, required this.domain, required this.userInfo}); /// Constructs an instance of this from a key-value map of data. /// diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.dart index d5a03ed2f8d3..6d6f241d6ca8 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.dart @@ -38,9 +38,7 @@ class AppStorePurchaseDetails extends PurchaseDetails { /// The status that this [PurchaseDetails] is currently on. PurchaseStatus get status => _status; set status(PurchaseStatus status) { - if (status == PurchaseStatus.purchased || status == PurchaseStatus.error) { - _pendingCompletePurchase = true; - } + _pendingCompletePurchase = status != PurchaseStatus.pending; _status = status; } diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/types.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/types.dart index fdca6a0fb12f..a21bd4b5fbb1 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/types.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/types.dart @@ -1,7 +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. -// +// export 'app_store_product_details.dart'; export 'app_store_purchase_details.dart'; export 'app_store_purchase_param.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart new file mode 100644 index 000000000000..dbae38285a2d --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart @@ -0,0 +1,504 @@ +// 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 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; +import 'package:in_app_purchase_ios/src/channel.dart'; +import 'package:in_app_purchase_ios/src/store_kit_wrappers/enum_converters.dart'; +import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +import 'store_kit_wrappers/sk_test_stub_objects.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final FakeIOSPlatform fakeIOSPlatform = FakeIOSPlatform(); + + setUpAll(() { + SystemChannels.platform + .setMockMethodCallHandler(fakeIOSPlatform.onMethodCall); + }); + + setUp(() => fakeIOSPlatform.reset()); + + tearDown(() => fakeIOSPlatform.reset()); + + group('isAvailable', () { + test('true', () async { + expect(await InAppPurchaseIosPlatform.instance.isAvailable(), isTrue); + }); + }); + + group('query product list', () { + test('should get product list and correct invalid identifiers', () async { + final InAppPurchaseIosPlatform connection = InAppPurchaseIosPlatform(); + final ProductDetailsResponse response = await connection + .queryProductDetails(['123', '456', '789'].toSet()); + List products = response.productDetails; + expect(products.first.id, '123'); + expect(products[1].id, '456'); + expect(response.notFoundIDs, ['789']); + expect(response.error, isNull); + }); + + test( + 'if query products throws error, should get error object in the response', + () async { + fakeIOSPlatform.queryProductException = PlatformException( + code: 'error_code', + message: 'error_message', + details: {'info': 'error_info'}); + final InAppPurchaseIosPlatform connection = InAppPurchaseIosPlatform(); + final ProductDetailsResponse response = await connection + .queryProductDetails(['123', '456', '789'].toSet()); + expect(response.productDetails, []); + expect(response.notFoundIDs, ['123', '456', '789']); + expect(response.error, isNotNull); + expect(response.error!.source, kIAPSource); + expect(response.error!.code, 'error_code'); + expect(response.error!.message, 'error_message'); + expect(response.error!.details, {'info': 'error_info'}); + }); + }); + + group('restore purchases', () { + test('should emit restored transactions on purchase stream', () async { + Completer completer = Completer(); + Stream> stream = + InAppPurchaseIosPlatform.instance.purchaseStream; + + late StreamSubscription subscription; + subscription = stream.listen((purchaseDetailsList) { + if (purchaseDetailsList.first.status == PurchaseStatus.restored) { + completer.complete(purchaseDetailsList); + subscription.cancel(); + } + }); + + await InAppPurchaseIosPlatform.instance.restorePurchases(); + List details = await completer.future; + + expect(details.length, 2); + for (int i = 0; i < fakeIOSPlatform.transactions.length; i++) { + SKPaymentTransactionWrapper expected = fakeIOSPlatform.transactions[i]; + PurchaseDetails actual = details[i]; + + expect(actual.purchaseID, expected.transactionIdentifier); + expect(actual.verificationData, isNotNull); + expect(actual.status, PurchaseStatus.restored); + expect(actual.verificationData.localVerificationData, + fakeIOSPlatform.receiptData); + expect(actual.verificationData.serverVerificationData, + fakeIOSPlatform.receiptData); + expect(actual.pendingCompletePurchase, true); + } + }); + + test('should not block transaction updates', () async { + fakeIOSPlatform.transactions + .insert(0, fakeIOSPlatform.createPurchasedTransaction('foo', 'bar')); + Completer completer = Completer(); + Stream> stream = + InAppPurchaseIosPlatform.instance.purchaseStream; + + late StreamSubscription subscription; + subscription = stream.listen((purchaseDetailsList) { + if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { + completer.complete(purchaseDetailsList); + subscription.cancel(); + } + }); + await InAppPurchaseIosPlatform.instance.restorePurchases(); + List details = await completer.future; + expect(details.length, 3); + for (int i = 0; i < fakeIOSPlatform.transactions.length; i++) { + SKPaymentTransactionWrapper expected = fakeIOSPlatform.transactions[i]; + PurchaseDetails actual = details[i]; + + expect(actual.purchaseID, expected.transactionIdentifier); + expect(actual.verificationData, isNotNull); + expect( + actual.status, + SKTransactionStatusConverter() + .toPurchaseStatus(expected.transactionState), + ); + expect(actual.verificationData.localVerificationData, + fakeIOSPlatform.receiptData); + expect(actual.verificationData.serverVerificationData, + fakeIOSPlatform.receiptData); + expect(actual.pendingCompletePurchase, true); + } + }); + + test('receipt error should populate null to verificationData.data', + () async { + fakeIOSPlatform.receiptData = null; + Completer completer = Completer(); + Stream> stream = + InAppPurchaseIosPlatform.instance.purchaseStream; + + late StreamSubscription subscription; + subscription = stream.listen((purchaseDetailsList) { + if (purchaseDetailsList.first.status == PurchaseStatus.restored) { + completer.complete(purchaseDetailsList); + subscription.cancel(); + } + }); + + await InAppPurchaseIosPlatform.instance.restorePurchases(); + List details = await completer.future; + + for (PurchaseDetails purchase in details) { + expect(purchase.verificationData.localVerificationData, isEmpty); + expect(purchase.verificationData.serverVerificationData, isEmpty); + } + }); + + test('test restore error', () { + fakeIOSPlatform.testRestoredError = SKError( + code: 123, + domain: 'error_test', + userInfo: {'message': 'errorMessage'}); + + expect( + () => InAppPurchaseIosPlatform.instance.restorePurchases(), + throwsA( + isA() + .having((error) => error.code, 'code', 123) + .having((error) => error.domain, 'domain', 'error_test') + .having((error) => error.userInfo, 'userInfo', + {'message': 'errorMessage'}), + )); + }); + }); + + /* + group('refresh receipt data', () { + test('should refresh receipt data', () async { + PurchaseVerificationData? receiptData = await InAppPurchaseIosPlatform + .instance + .refreshPurchaseVerificationData(); + expect(receiptData, isNotNull); + expect(receiptData!.source, IAPSource.AppStore); + expect(receiptData.localVerificationData, 'refreshed receipt data'); + expect(receiptData.serverVerificationData, 'refreshed receipt data'); + }); + }); + */ + + group('make payment', () { + test( + 'buying non consumable, should get purchase objects in the purchase update callback', + () async { + List details = []; + Completer completer = Completer(); + Stream> stream = + InAppPurchaseIosPlatform.instance.purchaseStream; + + late StreamSubscription subscription; + subscription = stream.listen((purchaseDetailsList) { + details.addAll(purchaseDetailsList); + if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { + completer.complete(details); + subscription.cancel(); + } + }); + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: + AppStoreProductDetails.fromSKProduct(dummyProductWrapper), + applicationUserName: 'appName'); + await InAppPurchaseIosPlatform.instance + .buyNonConsumable(purchaseParam: purchaseParam); + + List result = await completer.future; + expect(result.length, 2); + expect(result.first.productID, dummyProductWrapper.productIdentifier); + }); + + test( + 'buying consumable, should get purchase objects in the purchase update callback', + () async { + List details = []; + Completer completer = Completer(); + Stream> stream = + InAppPurchaseIosPlatform.instance.purchaseStream; + + late StreamSubscription subscription; + subscription = stream.listen((purchaseDetailsList) { + details.addAll(purchaseDetailsList); + if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { + completer.complete(details); + subscription.cancel(); + } + }); + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: + AppStoreProductDetails.fromSKProduct(dummyProductWrapper), + applicationUserName: 'appName'); + await InAppPurchaseIosPlatform.instance + .buyConsumable(purchaseParam: purchaseParam); + + List result = await completer.future; + expect(result.length, 2); + expect(result.first.productID, dummyProductWrapper.productIdentifier); + }); + + test('buying consumable, should throw when autoConsume is false', () async { + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: + AppStoreProductDetails.fromSKProduct(dummyProductWrapper), + applicationUserName: 'appName'); + expect( + () => InAppPurchaseIosPlatform.instance + .buyConsumable(purchaseParam: purchaseParam, autoConsume: false), + throwsA(isInstanceOf())); + }); + + test('should get failed purchase status', () async { + fakeIOSPlatform.testTransactionFail = true; + List details = []; + Completer completer = Completer(); + late IAPError error; + + Stream> stream = + InAppPurchaseIosPlatform.instance.purchaseStream; + late StreamSubscription subscription; + subscription = stream.listen((purchaseDetailsList) { + details.addAll(purchaseDetailsList); + purchaseDetailsList.forEach((purchaseDetails) { + if (purchaseDetails.status == PurchaseStatus.error) { + error = purchaseDetails.error!; + completer.complete(error); + subscription.cancel(); + } + }); + }); + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: + AppStoreProductDetails.fromSKProduct(dummyProductWrapper), + applicationUserName: 'appName'); + await InAppPurchaseIosPlatform.instance + .buyNonConsumable(purchaseParam: purchaseParam); + + IAPError completerError = await completer.future; + expect(completerError.code, 'purchase_error'); + expect(completerError.source, kIAPSource); + expect(completerError.message, 'ios_domain'); + expect(completerError.details, {'message': 'an error message'}); + }); + }); + + group('complete purchase', () { + test('should complete purchase', () async { + List details = []; + Completer completer = Completer(); + Stream> stream = + InAppPurchaseIosPlatform.instance.purchaseStream; + late StreamSubscription subscription; + subscription = stream.listen((purchaseDetailsList) { + details.addAll(purchaseDetailsList); + purchaseDetailsList.forEach((purchaseDetails) { + if (purchaseDetails.pendingCompletePurchase) { + InAppPurchaseIosPlatform.instance.completePurchase(purchaseDetails); + completer.complete(details); + subscription.cancel(); + } + }); + }); + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: + AppStoreProductDetails.fromSKProduct(dummyProductWrapper), + applicationUserName: 'appName'); + await InAppPurchaseIosPlatform.instance + .buyNonConsumable(purchaseParam: purchaseParam); + List result = await completer.future; + expect(result.length, 2); + expect(result.first.productID, dummyProductWrapper.productIdentifier); + expect(fakeIOSPlatform.finishedTransactions.length, 1); + }); + }); + + /* + group('present code redemption sheet', () { + test('null', () async { + expect( + await InAppPurchaseIosPlatform.instance.presentCodeRedemptionSheet(), + null); + }); + }); + */ +} + +class FakeIOSPlatform { + FakeIOSPlatform() { + channel.setMockMethodCallHandler(onMethodCall); + } + + // pre-configured store informations + String? receiptData; + late Set validProductIDs; + late Map validProducts; + late List transactions; + late List finishedTransactions; + late bool testRestoredTransactionsNull; + late bool testTransactionFail; + PlatformException? queryProductException; + PlatformException? restoreException; + SKError? testRestoredError; + + void reset() { + transactions = []; + receiptData = 'dummy base64data'; + validProductIDs = ['123', '456'].toSet(); + validProducts = Map(); + for (String validID in validProductIDs) { + Map productWrapperMap = + buildProductMap(dummyProductWrapper); + productWrapperMap['productIdentifier'] = validID; + validProducts[validID] = SKProductWrapper.fromJson(productWrapperMap); + } + + SKPaymentTransactionWrapper tran1 = SKPaymentTransactionWrapper( + transactionIdentifier: '123', + payment: dummyPayment, + originalTransaction: dummyTransaction, + transactionTimeStamp: 123123123.022, + transactionState: SKPaymentTransactionStateWrapper.restored, + error: null, + ); + SKPaymentTransactionWrapper tran2 = SKPaymentTransactionWrapper( + transactionIdentifier: '1234', + payment: dummyPayment, + originalTransaction: dummyTransaction, + transactionTimeStamp: 123123123.022, + transactionState: SKPaymentTransactionStateWrapper.restored, + error: null, + ); + + transactions.addAll([tran1, tran2]); + finishedTransactions = []; + testRestoredTransactionsNull = false; + testTransactionFail = false; + queryProductException = null; + restoreException = null; + testRestoredError = null; + } + + SKPaymentTransactionWrapper createPendingTransaction(String id) { + return SKPaymentTransactionWrapper( + transactionIdentifier: '', + payment: SKPaymentWrapper(productIdentifier: id), + transactionState: SKPaymentTransactionStateWrapper.purchasing, + transactionTimeStamp: 123123.121, + error: null, + originalTransaction: null); + } + + SKPaymentTransactionWrapper createPurchasedTransaction( + String productId, String transactionId) { + return SKPaymentTransactionWrapper( + payment: SKPaymentWrapper(productIdentifier: productId), + transactionState: SKPaymentTransactionStateWrapper.purchased, + transactionTimeStamp: 123123.121, + transactionIdentifier: transactionId, + error: null, + originalTransaction: null); + } + + SKPaymentTransactionWrapper createFailedTransaction(String productId) { + return SKPaymentTransactionWrapper( + transactionIdentifier: '', + payment: SKPaymentWrapper(productIdentifier: productId), + transactionState: SKPaymentTransactionStateWrapper.failed, + transactionTimeStamp: 123123.121, + error: SKError( + code: 0, + domain: 'ios_domain', + userInfo: {'message': 'an error message'}), + originalTransaction: null); + } + + Future onMethodCall(MethodCall call) { + switch (call.method) { + case '-[SKPaymentQueue canMakePayments:]': + return Future.value(true); + case '-[InAppPurchasePlugin startProductRequest:result:]': + if (queryProductException != null) { + throw queryProductException!; + } + List productIDS = + List.castFrom(call.arguments); + assert(productIDS is List, 'invalid argument type'); + List invalidFound = []; + List products = []; + for (String productID in productIDS) { + if (!validProductIDs.contains(productID)) { + invalidFound.add(productID); + } else { + products.add(validProducts[productID]!); + } + } + SkProductResponseWrapper response = SkProductResponseWrapper( + products: products, invalidProductIdentifiers: invalidFound); + return Future>.value( + buildProductResponseMap(response)); + case '-[InAppPurchasePlugin restoreTransactions:result:]': + if (restoreException != null) { + throw restoreException!; + } + if (testRestoredError != null) { + InAppPurchaseIosPlatform.observer + .restoreCompletedTransactionsFailed(error: testRestoredError!); + return Future.sync(() {}); + } + if (!testRestoredTransactionsNull) { + InAppPurchaseIosPlatform.observer + .updatedTransactions(transactions: transactions); + } + InAppPurchaseIosPlatform.observer + .paymentQueueRestoreCompletedTransactionsFinished(); + + return Future.sync(() {}); + case '-[InAppPurchasePlugin retrieveReceiptData:result:]': + if (receiptData != null) { + return Future.value(receiptData); + } else { + throw PlatformException(code: 'no_receipt_data'); + } + case '-[InAppPurchasePlugin refreshReceipt:result:]': + receiptData = 'refreshed receipt data'; + return Future.sync(() {}); + case '-[InAppPurchasePlugin addPayment:result:]': + String id = call.arguments['productIdentifier']; + SKPaymentTransactionWrapper transaction = createPendingTransaction(id); + InAppPurchaseIosPlatform.observer + .updatedTransactions(transactions: [transaction]); + sleep(const Duration(milliseconds: 30)); + if (testTransactionFail) { + SKPaymentTransactionWrapper transaction_failed = + createFailedTransaction(id); + InAppPurchaseIosPlatform.observer + .updatedTransactions(transactions: [transaction_failed]); + } else { + SKPaymentTransactionWrapper transaction_finished = + createPurchasedTransaction( + id, transaction.transactionIdentifier ?? ''); + InAppPurchaseIosPlatform.observer + .updatedTransactions(transactions: [transaction_finished]); + } + break; + case '-[InAppPurchasePlugin finishTransaction:result:]': + finishedTransactions.add(createPurchasedTransaction( + call.arguments["productIdentifier"], + call.arguments["transactionIdentifier"])); + break; + } + return Future.sync(() {}); + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart index e707674b1935..9454a9d4ebee 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart @@ -79,7 +79,8 @@ void main() { test('toProductDetails() should return correct Product object', () { final SKProductWrapper wrapper = SKProductWrapper.fromJson(buildProductMap(dummyProductWrapper)); - final AppStoreProductDetails product = AppStoreProductDetails.fromSKProduct(wrapper); + final AppStoreProductDetails product = + AppStoreProductDetails.fromSKProduct(wrapper); expect(product.title, wrapper.localizedTitle); expect(product.description, wrapper.localizedDescription); expect(product.id, wrapper.productIdentifier); @@ -134,7 +135,8 @@ void main() { test('toPurchaseDetails() should return correct PurchaseDetail object', () { AppStorePurchaseDetails details = - AppStorePurchaseDetails.fromSKTransaction(dummyTransaction, 'receipt data'); + AppStorePurchaseDetails.fromSKTransaction( + dummyTransaction, 'receipt data'); expect(dummyTransaction.transactionIdentifier, details.purchaseID); expect(dummyTransaction.payment.productIdentifier, details.productID); expect(dummyTransaction.transactionTimeStamp, isNotNull); 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 983fb2df53c1..892f8742f52c 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,8 +3,8 @@ // found in the LICENSE file. /// The class represents the information of a product. -/// -/// +/// +/// class ProductDetails { /// Creates a new product details object with the provided details. ProductDetails({ From 2f115b982eb8b8463a9b8f2cd40197191f3c2ca5 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 28 Apr 2021 14:03:16 +0200 Subject: [PATCH 22/33] Clean up code --- .../lib/src/in_app_purchase_ios_platform.dart | 21 ---------------- .../in_app_purchase_ios_platform_test.dart | 24 ------------------- 2 files changed, 45 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart index b9534c6d18c0..57c26bfb0b1f 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart @@ -153,27 +153,6 @@ class InAppPurchaseIosPlatform implements InAppPurchasePlatform { ); return productDetailsResponse; } - - // TODO(mvanbeusekom): Move into `InAppPurchaseIosAddition` class... - /* - @override - Future refreshPurchaseVerificationData() async { - await SKRequestMaker().startRefreshReceiptRequest(); - final String? receipt = await SKReceiptManager.retrieveReceiptData(); - if (receipt == null) { - return null; - } - return PurchaseVerificationData( - localVerificationData: receipt, - serverVerificationData: receipt, - source: kIAPSource); - } - - @override - Future presentCodeRedemptionSheet() { - return _skPaymentQueueWrapper.presentCodeRedemptionSheet(); - } - */ } class _TransactionObserver implements SKTransactionObserverWrapper { diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart index dbae38285a2d..aff94b62e37e 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart @@ -178,20 +178,6 @@ void main() { }); }); - /* - group('refresh receipt data', () { - test('should refresh receipt data', () async { - PurchaseVerificationData? receiptData = await InAppPurchaseIosPlatform - .instance - .refreshPurchaseVerificationData(); - expect(receiptData, isNotNull); - expect(receiptData!.source, IAPSource.AppStore); - expect(receiptData.localVerificationData, 'refreshed receipt data'); - expect(receiptData.serverVerificationData, 'refreshed receipt data'); - }); - }); - */ - group('make payment', () { test( 'buying non consumable, should get purchase objects in the purchase update callback', @@ -323,16 +309,6 @@ void main() { expect(fakeIOSPlatform.finishedTransactions.length, 1); }); }); - - /* - group('present code redemption sheet', () { - test('null', () async { - expect( - await InAppPurchaseIosPlatform.instance.presentCodeRedemptionSheet(), - null); - }); - }); - */ } class FakeIOSPlatform { From d4e916594b9193ab2058b668a53e1a3c2537fd97 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 28 Apr 2021 15:07:24 +0200 Subject: [PATCH 23/33] Added initial version of README.md --- .../in_app_purchase_ios/README.md | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 packages/in_app_purchase/in_app_purchase_ios/README.md diff --git a/packages/in_app_purchase/in_app_purchase_ios/README.md b/packages/in_app_purchase/in_app_purchase_ios/README.md new file mode 100644 index 000000000000..d29ec2af36e2 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/README.md @@ -0,0 +1,34 @@ +# in_app_purchase_ios + +The iOS implementation of [`in_app_purchase`][1]. + +## Usage + +### Import the package + +This package has been endorsed, meaning that you only need to add `in_app_purchase` +as a dependency in your `pubspec.yaml`. It will be automatically included in your app +when you depend on `package:in_app_purchase`. + +This is what the above means to your `pubspec.yaml`: + +```yaml +... +dependencies: + ... + in_app_purchase: ^0.6.0 + ... +``` + +If you wish to use the iOS package only, you can add `url_launcher_ios` as a +dependency: + +```yaml +... +dependencies: + ... + in_app_purchase_ios: ^1.0.0 + ... +``` + +[1]: ../in_app_purchase/in_app_purchase \ No newline at end of file From 61dc0be2af42cbc02ea7fd80f61ff155fb6a0731 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 28 Apr 2021 16:38:05 +0200 Subject: [PATCH 24/33] Fixed typo --- packages/in_app_purchase/in_app_purchase_ios/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/README.md b/packages/in_app_purchase/in_app_purchase_ios/README.md index d29ec2af36e2..025ed36b72a6 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/README.md +++ b/packages/in_app_purchase/in_app_purchase_ios/README.md @@ -20,7 +20,7 @@ dependencies: ... ``` -If you wish to use the iOS package only, you can add `url_launcher_ios` as a +If you wish to use the iOS package only, you can add `in_app_purchase_ios` as a dependency: ```yaml From a33f3123e51495269c9874e991321e527bdee1a4 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 28 Apr 2021 17:25:08 +0200 Subject: [PATCH 25/33] Added support for iOS specific features --- .../lib/in_app_purchase_ios.dart | 1 + .../lib/src/in_app_purchase_ios_platform.dart | 9 +- ...in_app_purchase_ios_platform_addition.dart | 29 +++ .../test/fakes/fake_ios_platform.dart | 182 ++++++++++++++++++ ...pp_purchase_ios_platform_addtion_test.dart | 37 ++++ .../in_app_purchase_ios_platform_test.dart | 171 +--------------- 6 files changed, 258 insertions(+), 171 deletions(-) create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_addtion_test.dart diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/in_app_purchase_ios.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/in_app_purchase_ios.dart index 0cd8c22ba094..21e76815e6ac 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/in_app_purchase_ios.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/in_app_purchase_ios.dart @@ -3,4 +3,5 @@ // found in the LICENSE file. export 'src/in_app_purchase_ios_platform.dart'; +export 'src/in_app_purchase_ios_platform_addition.dart'; export 'src/types/types.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart index 57c26bfb0b1f..57b401439ab9 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:in_app_purchase_ios/src/in_app_purchase_ios_platform_addition.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import '../in_app_purchase_ios.dart'; @@ -27,7 +28,7 @@ final String kIAPSource = 'app_store'; /// /// This translates various `StoreKit` calls and responses into the /// generic plugin API. -class InAppPurchaseIosPlatform implements InAppPurchasePlatform { +class InAppPurchaseIosPlatform extends InAppPurchasePlatform { /// Returns the singleton instance of the [InAppPurchaseIosPlatform] that should be /// used across the app. static InAppPurchaseIosPlatform get instance => _getOrCreateInstance(); @@ -54,6 +55,12 @@ class InAppPurchaseIosPlatform implements InAppPurchasePlatform { return _instance!; } + // Register the [InAppPurchaseIosPlatformAddition] containing iOS + // platform-specific functionality. + InAppPurchasePlatformAddition.instance = InAppPurchaseIosPlatformAddition(); + + // Register the platform-specific implementation of the idiomatic + // InAppPurchase API. _instance = InAppPurchaseIosPlatform(); _skPaymentQueueWrapper = SKPaymentQueueWrapper(); _observer = _TransactionObserver(StreamController.broadcast()); diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart new file mode 100644 index 000000000000..ce281780f25a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart @@ -0,0 +1,29 @@ +import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +import '../store_kit_wrappers.dart'; + +/// Contains InApp Purchase features that are only available on iOS. +class InAppPurchaseIosPlatformAddition extends InAppPurchasePlatformAddition { + /// Present Code Redemption Sheet. + /// + /// Available on devices running iOS 14 and iPadOS 14 and later. + Future presentCodeRedemptionSheet() { + return SKPaymentQueueWrapper().presentCodeRedemptionSheet(); + } + + /// Retry loading purchase data after an initial failure. + /// + /// If no results, a `null` value is returned. + Future refreshPurchaseVerificationData() async { + await SKRequestMaker().startRefreshReceiptRequest(); + final String? receipt = await SKReceiptManager.retrieveReceiptData(); + if (receipt == null) { + return null; + } + return PurchaseVerificationData( + localVerificationData: receipt, + serverVerificationData: receipt, + source: kIAPSource); + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart b/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart new file mode 100644 index 000000000000..f39241318670 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart @@ -0,0 +1,182 @@ +// 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 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; +import 'package:in_app_purchase_ios/src/channel.dart'; +import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; + +import '../store_kit_wrappers/sk_test_stub_objects.dart'; + +class FakeIOSPlatform { + FakeIOSPlatform() { + channel.setMockMethodCallHandler(onMethodCall); + } + + // pre-configured store informations + String? receiptData; + late Set validProductIDs; + late Map validProducts; + late List transactions; + late List finishedTransactions; + late bool testRestoredTransactionsNull; + late bool testTransactionFail; + PlatformException? queryProductException; + PlatformException? restoreException; + SKError? testRestoredError; + + void reset() { + transactions = []; + receiptData = 'dummy base64data'; + validProductIDs = ['123', '456'].toSet(); + validProducts = Map(); + for (String validID in validProductIDs) { + Map productWrapperMap = + buildProductMap(dummyProductWrapper); + productWrapperMap['productIdentifier'] = validID; + validProducts[validID] = SKProductWrapper.fromJson(productWrapperMap); + } + + SKPaymentTransactionWrapper tran1 = SKPaymentTransactionWrapper( + transactionIdentifier: '123', + payment: dummyPayment, + originalTransaction: dummyTransaction, + transactionTimeStamp: 123123123.022, + transactionState: SKPaymentTransactionStateWrapper.restored, + error: null, + ); + SKPaymentTransactionWrapper tran2 = SKPaymentTransactionWrapper( + transactionIdentifier: '1234', + payment: dummyPayment, + originalTransaction: dummyTransaction, + transactionTimeStamp: 123123123.022, + transactionState: SKPaymentTransactionStateWrapper.restored, + error: null, + ); + + transactions.addAll([tran1, tran2]); + finishedTransactions = []; + testRestoredTransactionsNull = false; + testTransactionFail = false; + queryProductException = null; + restoreException = null; + testRestoredError = null; + } + + SKPaymentTransactionWrapper createPendingTransaction(String id) { + return SKPaymentTransactionWrapper( + transactionIdentifier: '', + payment: SKPaymentWrapper(productIdentifier: id), + transactionState: SKPaymentTransactionStateWrapper.purchasing, + transactionTimeStamp: 123123.121, + error: null, + originalTransaction: null); + } + + SKPaymentTransactionWrapper createPurchasedTransaction( + String productId, String transactionId) { + return SKPaymentTransactionWrapper( + payment: SKPaymentWrapper(productIdentifier: productId), + transactionState: SKPaymentTransactionStateWrapper.purchased, + transactionTimeStamp: 123123.121, + transactionIdentifier: transactionId, + error: null, + originalTransaction: null); + } + + SKPaymentTransactionWrapper createFailedTransaction(String productId) { + return SKPaymentTransactionWrapper( + transactionIdentifier: '', + payment: SKPaymentWrapper(productIdentifier: productId), + transactionState: SKPaymentTransactionStateWrapper.failed, + transactionTimeStamp: 123123.121, + error: SKError( + code: 0, + domain: 'ios_domain', + userInfo: {'message': 'an error message'}), + originalTransaction: null); + } + + Future onMethodCall(MethodCall call) { + switch (call.method) { + case '-[SKPaymentQueue canMakePayments:]': + return Future.value(true); + case '-[InAppPurchasePlugin startProductRequest:result:]': + if (queryProductException != null) { + throw queryProductException!; + } + List productIDS = + List.castFrom(call.arguments); + assert(productIDS is List, 'invalid argument type'); + List invalidFound = []; + List products = []; + for (String productID in productIDS) { + if (!validProductIDs.contains(productID)) { + invalidFound.add(productID); + } else { + products.add(validProducts[productID]!); + } + } + SkProductResponseWrapper response = SkProductResponseWrapper( + products: products, invalidProductIdentifiers: invalidFound); + return Future>.value( + buildProductResponseMap(response)); + case '-[InAppPurchasePlugin restoreTransactions:result:]': + if (restoreException != null) { + throw restoreException!; + } + if (testRestoredError != null) { + InAppPurchaseIosPlatform.observer + .restoreCompletedTransactionsFailed(error: testRestoredError!); + return Future.sync(() {}); + } + if (!testRestoredTransactionsNull) { + InAppPurchaseIosPlatform.observer + .updatedTransactions(transactions: transactions); + } + InAppPurchaseIosPlatform.observer + .paymentQueueRestoreCompletedTransactionsFinished(); + + return Future.sync(() {}); + case '-[InAppPurchasePlugin retrieveReceiptData:result:]': + if (receiptData != null) { + return Future.value(receiptData); + } else { + throw PlatformException(code: 'no_receipt_data'); + } + case '-[InAppPurchasePlugin refreshReceipt:result:]': + receiptData = 'refreshed receipt data'; + return Future.sync(() {}); + case '-[InAppPurchasePlugin addPayment:result:]': + String id = call.arguments['productIdentifier']; + SKPaymentTransactionWrapper transaction = createPendingTransaction(id); + InAppPurchaseIosPlatform.observer + .updatedTransactions(transactions: [transaction]); + sleep(const Duration(milliseconds: 30)); + if (testTransactionFail) { + SKPaymentTransactionWrapper transaction_failed = + createFailedTransaction(id); + InAppPurchaseIosPlatform.observer + .updatedTransactions(transactions: [transaction_failed]); + } else { + SKPaymentTransactionWrapper transaction_finished = + createPurchasedTransaction( + id, transaction.transactionIdentifier ?? ''); + InAppPurchaseIosPlatform.observer + .updatedTransactions(transactions: [transaction_finished]); + } + break; + case '-[InAppPurchasePlugin finishTransaction:result:]': + finishedTransactions.add(createPurchasedTransaction( + call.arguments["productIdentifier"], + call.arguments["transactionIdentifier"])); + break; + } + return Future.sync(() {}); + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_addtion_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_addtion_test.dart new file mode 100644 index 000000000000..dcd15332ae83 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_addtion_test.dart @@ -0,0 +1,37 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +import 'fakes/fake_ios_platform.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final FakeIOSPlatform fakeIOSPlatform = FakeIOSPlatform(); + + setUpAll(() { + SystemChannels.platform + .setMockMethodCallHandler(fakeIOSPlatform.onMethodCall); + }); + + group('present code redemption sheet', () { + test('null', () async { + expect( + await InAppPurchaseIosPlatformAddition().presentCodeRedemptionSheet(), + null); + }); + }); + + group('refresh receipt data', () { + test('should refresh receipt data', () async { + PurchaseVerificationData? receiptData = + await InAppPurchaseIosPlatformAddition() + .refreshPurchaseVerificationData(); + expect(receiptData, isNotNull); + expect(receiptData!.source, kIAPSource); + expect(receiptData.localVerificationData, 'refreshed receipt data'); + expect(receiptData.serverVerificationData, 'refreshed receipt data'); + }); + }); +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart index aff94b62e37e..a70e2d9191bb 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart @@ -3,16 +3,15 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; -import 'package:in_app_purchase_ios/src/channel.dart'; import 'package:in_app_purchase_ios/src/store_kit_wrappers/enum_converters.dart'; import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; +import 'fakes/fake_ios_platform.dart'; import 'store_kit_wrappers/sk_test_stub_objects.dart'; void main() { @@ -310,171 +309,3 @@ void main() { }); }); } - -class FakeIOSPlatform { - FakeIOSPlatform() { - channel.setMockMethodCallHandler(onMethodCall); - } - - // pre-configured store informations - String? receiptData; - late Set validProductIDs; - late Map validProducts; - late List transactions; - late List finishedTransactions; - late bool testRestoredTransactionsNull; - late bool testTransactionFail; - PlatformException? queryProductException; - PlatformException? restoreException; - SKError? testRestoredError; - - void reset() { - transactions = []; - receiptData = 'dummy base64data'; - validProductIDs = ['123', '456'].toSet(); - validProducts = Map(); - for (String validID in validProductIDs) { - Map productWrapperMap = - buildProductMap(dummyProductWrapper); - productWrapperMap['productIdentifier'] = validID; - validProducts[validID] = SKProductWrapper.fromJson(productWrapperMap); - } - - SKPaymentTransactionWrapper tran1 = SKPaymentTransactionWrapper( - transactionIdentifier: '123', - payment: dummyPayment, - originalTransaction: dummyTransaction, - transactionTimeStamp: 123123123.022, - transactionState: SKPaymentTransactionStateWrapper.restored, - error: null, - ); - SKPaymentTransactionWrapper tran2 = SKPaymentTransactionWrapper( - transactionIdentifier: '1234', - payment: dummyPayment, - originalTransaction: dummyTransaction, - transactionTimeStamp: 123123123.022, - transactionState: SKPaymentTransactionStateWrapper.restored, - error: null, - ); - - transactions.addAll([tran1, tran2]); - finishedTransactions = []; - testRestoredTransactionsNull = false; - testTransactionFail = false; - queryProductException = null; - restoreException = null; - testRestoredError = null; - } - - SKPaymentTransactionWrapper createPendingTransaction(String id) { - return SKPaymentTransactionWrapper( - transactionIdentifier: '', - payment: SKPaymentWrapper(productIdentifier: id), - transactionState: SKPaymentTransactionStateWrapper.purchasing, - transactionTimeStamp: 123123.121, - error: null, - originalTransaction: null); - } - - SKPaymentTransactionWrapper createPurchasedTransaction( - String productId, String transactionId) { - return SKPaymentTransactionWrapper( - payment: SKPaymentWrapper(productIdentifier: productId), - transactionState: SKPaymentTransactionStateWrapper.purchased, - transactionTimeStamp: 123123.121, - transactionIdentifier: transactionId, - error: null, - originalTransaction: null); - } - - SKPaymentTransactionWrapper createFailedTransaction(String productId) { - return SKPaymentTransactionWrapper( - transactionIdentifier: '', - payment: SKPaymentWrapper(productIdentifier: productId), - transactionState: SKPaymentTransactionStateWrapper.failed, - transactionTimeStamp: 123123.121, - error: SKError( - code: 0, - domain: 'ios_domain', - userInfo: {'message': 'an error message'}), - originalTransaction: null); - } - - Future onMethodCall(MethodCall call) { - switch (call.method) { - case '-[SKPaymentQueue canMakePayments:]': - return Future.value(true); - case '-[InAppPurchasePlugin startProductRequest:result:]': - if (queryProductException != null) { - throw queryProductException!; - } - List productIDS = - List.castFrom(call.arguments); - assert(productIDS is List, 'invalid argument type'); - List invalidFound = []; - List products = []; - for (String productID in productIDS) { - if (!validProductIDs.contains(productID)) { - invalidFound.add(productID); - } else { - products.add(validProducts[productID]!); - } - } - SkProductResponseWrapper response = SkProductResponseWrapper( - products: products, invalidProductIdentifiers: invalidFound); - return Future>.value( - buildProductResponseMap(response)); - case '-[InAppPurchasePlugin restoreTransactions:result:]': - if (restoreException != null) { - throw restoreException!; - } - if (testRestoredError != null) { - InAppPurchaseIosPlatform.observer - .restoreCompletedTransactionsFailed(error: testRestoredError!); - return Future.sync(() {}); - } - if (!testRestoredTransactionsNull) { - InAppPurchaseIosPlatform.observer - .updatedTransactions(transactions: transactions); - } - InAppPurchaseIosPlatform.observer - .paymentQueueRestoreCompletedTransactionsFinished(); - - return Future.sync(() {}); - case '-[InAppPurchasePlugin retrieveReceiptData:result:]': - if (receiptData != null) { - return Future.value(receiptData); - } else { - throw PlatformException(code: 'no_receipt_data'); - } - case '-[InAppPurchasePlugin refreshReceipt:result:]': - receiptData = 'refreshed receipt data'; - return Future.sync(() {}); - case '-[InAppPurchasePlugin addPayment:result:]': - String id = call.arguments['productIdentifier']; - SKPaymentTransactionWrapper transaction = createPendingTransaction(id); - InAppPurchaseIosPlatform.observer - .updatedTransactions(transactions: [transaction]); - sleep(const Duration(milliseconds: 30)); - if (testTransactionFail) { - SKPaymentTransactionWrapper transaction_failed = - createFailedTransaction(id); - InAppPurchaseIosPlatform.observer - .updatedTransactions(transactions: [transaction_failed]); - } else { - SKPaymentTransactionWrapper transaction_finished = - createPurchasedTransaction( - id, transaction.transactionIdentifier ?? ''); - InAppPurchaseIosPlatform.observer - .updatedTransactions(transactions: [transaction_finished]); - } - break; - case '-[InAppPurchasePlugin finishTransaction:result:]': - finishedTransactions.add(createPurchasedTransaction( - call.arguments["productIdentifier"], - call.arguments["transactionIdentifier"])); - break; - } - return Future.sync(() {}); - } -} From 415ac5efd94aed3f5479f3546699d1c75c022c8e Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 28 Apr 2021 18:54:07 +0200 Subject: [PATCH 26/33] Add missing license headers --- .../lib/src/in_app_purchase_ios_platform_addition.dart | 4 ++++ .../test/in_app_purchase_ios_platform_addtion_test.dart | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart index ce281780f25a..0c7b2de860b6 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart @@ -1,3 +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 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_addtion_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_addtion_test.dart index dcd15332ae83..f8b75195fc6e 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_addtion_test.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_addtion_test.dart @@ -1,3 +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 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; From 70e89cd9f93b6800cb64be394919118a810f59fb Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 29 Apr 2021 10:05:50 +0200 Subject: [PATCH 27/33] Added feedback from PR --- .../in_app_purchase_ios/CHANGELOG.md | 2 +- .../ios/in_app_purchase.podspec | 4 +-- .../lib/src/in_app_purchase_ios_platform.dart | 31 ++++++------------- .../src/types/app_store_purchase_param.dart | 5 +++ .../in_app_purchase_ios/pubspec.yaml | 4 +-- 5 files changed, 20 insertions(+), 26 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md index 2f529b31655d..d46c124b9011 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md @@ -1,3 +1,3 @@ -## 1.0.0 +## 0.1.0 * Initial open-source release. \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase.podspec b/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase.podspec index 8da9d7894380..858dc018d1c6 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase.podspec +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase.podspec @@ -12,8 +12,8 @@ Downloaded by pub (not CocoaPods). s.homepage = 'https://github.com/flutter/plugins' s.license = { :type => 'BSD', :file => '../LICENSE' } s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/in_app_purchase' } - s.documentation_url = 'https://pub.dev/packages/in_app_purchase' + s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios' } + s.documentation_url = 'https://pub.dev/packages/in_app_purchase_ios' s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart index 57b401439ab9..bb2fd2b3639a 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart @@ -13,16 +13,10 @@ import '../in_app_purchase_ios.dart'; import '../store_kit_wrappers.dart'; /// [IAPError.code] code for failed purchases. -final String kPurchaseErrorCode = 'purchase_error'; - -/// [IAPError.code] code used when a query for previouys transaction has failed. -final String kRestoredPurchaseErrorCode = 'restore_transactions_failed'; - -/// [IAPError.code] code used when a consuming a purchased item fails. -final String kConsumptionFailedErrorCode = 'consume_purchase_failed'; +const String kPurchaseErrorCode = 'purchase_error'; /// Indicates store front is Apple AppStore. -final String kIAPSource = 'app_store'; +const String kIAPSource = 'app_store'; /// An [InAppPurchasePlatform] that wraps StoreKit. /// @@ -73,17 +67,13 @@ class InAppPurchaseIosPlatform extends InAppPurchasePlatform { @override Future buyNonConsumable({required PurchaseParam purchaseParam}) async { - if (!(purchaseParam is AppStorePurchaseParam)) { - throw ArgumentError( - 'On iOS, the `purchaseParam` should always be of type `AppStorePurchaseParam`.', - ); - } - await _skPaymentQueueWrapper.addPayment(SKPaymentWrapper( productIdentifier: purchaseParam.productDetails.id, quantity: 1, applicationUsername: purchaseParam.applicationUserName, - simulatesAskToBuyInSandbox: purchaseParam.simulatesAskToBuyInSandbox, + simulatesAskToBuyInSandbox: (purchaseParam is AppStorePurchaseParam) + ? purchaseParam.simulatesAskToBuyInSandbox + : false, requestData: null)); return true; // There's no error feedback from iOS here to return. @@ -98,14 +88,13 @@ class InAppPurchaseIosPlatform extends InAppPurchasePlatform { @override Future completePurchase(PurchaseDetails purchase) { - if (!(purchase is AppStorePurchaseDetails)) { - throw ArgumentError( - 'On iOS, the `purchase` should always be of type `AppStorePurchaseDetails`.', - ); - } + assert( + purchase is AppStorePurchaseDetails, + 'On iOS, the `purchase` should always be of type `AppStorePurchaseDetails`.', + ); return _skPaymentQueueWrapper.finishTransaction( - purchase.skPaymentTransaction, + (purchase as AppStorePurchaseDetails).skPaymentTransaction, ); } diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_param.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_param.dart index b825eeadf5a3..b2d8eea9d791 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_param.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_param.dart @@ -21,6 +21,11 @@ class AppStorePurchaseParam extends PurchaseParam { /// Set it to `true` to produce an "ask to buy" flow for this payment in the /// sandbox. /// + /// If you want to test [simulatesAskToBuyInSandbox], you should ensure that + /// you create an instance of the [AppStorePurchaseParam] class and set its + /// [simulateAskToBuyInSandbox] field to `true` and use it with the + /// `buyNonConsumable` or `buyConsumable` methods. + /// /// See also [SKPaymentWrapper.simulatesAskToBuyInSandbox]. final bool simulatesAskToBuyInSandbox; } diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml index 034567bd1ef6..a5351530f019 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -1,7 +1,7 @@ name: in_app_purchase_ios description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the iOS StoreKit Framework. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios -version: 1.0.0 +version: 0.1.0 # TODO(mvanbeusekom): Remove when in_app_purchase_platform_interface is published publish_to: 'none' @@ -31,4 +31,4 @@ dev_dependencies: environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" \ No newline at end of file + flutter: ">=1.20.0" From 0474545d80de0c677426cfd430dee21c74686b58 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 30 Apr 2021 08:58:34 +0200 Subject: [PATCH 28/33] Unblock tree by solving lint error --- .../in_app_purchase_ios/ios/in_app_purchase.podspec | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase.podspec b/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase.podspec index 858dc018d1c6..4a423dd036cf 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase.podspec +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase.podspec @@ -13,7 +13,9 @@ Downloaded by pub (not CocoaPods). s.license = { :type => 'BSD', :file => '../LICENSE' } s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios' } - s.documentation_url = 'https://pub.dev/packages/in_app_purchase_ios' + # TODO(mvanbeusekom): update URL when in_app_purchase_ios package is published. + # Updating it before the package is published will cause a lint error and block the tree. + s.documentation_url = 'https://pub.dev/packages/in_app_purchase' s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' From 125c490cb2d12944adf81a2f76ef49326a2ec8b6 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 5 May 2021 12:02:35 +0200 Subject: [PATCH 29/33] Implement registerPlatform method for iOS implementation --- .../lib/src/in_app_purchase_ios_platform.dart | 15 ++--- .../in_app_purchase_ios_platform_test.dart | 56 +++++++++---------- 2 files changed, 29 insertions(+), 42 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart index bb2fd2b3639a..5a2c40821007 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart @@ -23,10 +23,6 @@ const String kIAPSource = 'app_store'; /// This translates various `StoreKit` calls and responses into the /// generic plugin API. class InAppPurchaseIosPlatform extends InAppPurchasePlatform { - /// Returns the singleton instance of the [InAppPurchaseIosPlatform] that should be - /// used across the app. - static InAppPurchaseIosPlatform get instance => _getOrCreateInstance(); - static InAppPurchaseIosPlatform? _instance; static late SKPaymentQueueWrapper _skPaymentQueueWrapper; static late _TransactionObserver _observer; @@ -44,22 +40,19 @@ class InAppPurchaseIosPlatform extends InAppPurchasePlatform { @visibleForTesting static SKTransactionObserverWrapper get observer => _observer; - static InAppPurchaseIosPlatform _getOrCreateInstance() { - if (_instance != null) { - return _instance!; - } - + /// Registers this class as the default instance of [InAppPurchasePlatform]. + static void registerPlatform() { // Register the [InAppPurchaseIosPlatformAddition] containing iOS // platform-specific functionality. InAppPurchasePlatformAddition.instance = InAppPurchaseIosPlatformAddition(); // Register the platform-specific implementation of the idiomatic // InAppPurchase API. - _instance = InAppPurchaseIosPlatform(); + InAppPurchasePlatform.setInstance(InAppPurchaseIosPlatform()); + _skPaymentQueueWrapper = SKPaymentQueueWrapper(); _observer = _TransactionObserver(StreamController.broadcast()); _skPaymentQueueWrapper.setTransactionObserver(observer); - return _instance!; } @override diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart index a70e2d9191bb..b15249c81947 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart @@ -18,19 +18,24 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final FakeIOSPlatform fakeIOSPlatform = FakeIOSPlatform(); + late InAppPurchaseIosPlatform iapIosPlatform; setUpAll(() { SystemChannels.platform .setMockMethodCallHandler(fakeIOSPlatform.onMethodCall); }); - setUp(() => fakeIOSPlatform.reset()); + setUp(() { + InAppPurchaseIosPlatform.registerPlatform(); + iapIosPlatform = InAppPurchasePlatform.instance as InAppPurchaseIosPlatform; + fakeIOSPlatform.reset(); + }); tearDown(() => fakeIOSPlatform.reset()); group('isAvailable', () { test('true', () async { - expect(await InAppPurchaseIosPlatform.instance.isAvailable(), isTrue); + expect(await iapIosPlatform.isAvailable(), isTrue); }); }); @@ -69,8 +74,7 @@ void main() { group('restore purchases', () { test('should emit restored transactions on purchase stream', () async { Completer completer = Completer(); - Stream> stream = - InAppPurchaseIosPlatform.instance.purchaseStream; + Stream> stream = iapIosPlatform.purchaseStream; late StreamSubscription subscription; subscription = stream.listen((purchaseDetailsList) { @@ -80,7 +84,7 @@ void main() { } }); - await InAppPurchaseIosPlatform.instance.restorePurchases(); + await iapIosPlatform.restorePurchases(); List details = await completer.future; expect(details.length, 2); @@ -103,8 +107,7 @@ void main() { fakeIOSPlatform.transactions .insert(0, fakeIOSPlatform.createPurchasedTransaction('foo', 'bar')); Completer completer = Completer(); - Stream> stream = - InAppPurchaseIosPlatform.instance.purchaseStream; + Stream> stream = iapIosPlatform.purchaseStream; late StreamSubscription subscription; subscription = stream.listen((purchaseDetailsList) { @@ -113,7 +116,7 @@ void main() { subscription.cancel(); } }); - await InAppPurchaseIosPlatform.instance.restorePurchases(); + await iapIosPlatform.restorePurchases(); List details = await completer.future; expect(details.length, 3); for (int i = 0; i < fakeIOSPlatform.transactions.length; i++) { @@ -139,8 +142,7 @@ void main() { () async { fakeIOSPlatform.receiptData = null; Completer completer = Completer(); - Stream> stream = - InAppPurchaseIosPlatform.instance.purchaseStream; + Stream> stream = iapIosPlatform.purchaseStream; late StreamSubscription subscription; subscription = stream.listen((purchaseDetailsList) { @@ -150,7 +152,7 @@ void main() { } }); - await InAppPurchaseIosPlatform.instance.restorePurchases(); + await iapIosPlatform.restorePurchases(); List details = await completer.future; for (PurchaseDetails purchase in details) { @@ -166,7 +168,7 @@ void main() { userInfo: {'message': 'errorMessage'}); expect( - () => InAppPurchaseIosPlatform.instance.restorePurchases(), + () => iapIosPlatform.restorePurchases(), throwsA( isA() .having((error) => error.code, 'code', 123) @@ -183,8 +185,7 @@ void main() { () async { List details = []; Completer completer = Completer(); - Stream> stream = - InAppPurchaseIosPlatform.instance.purchaseStream; + Stream> stream = iapIosPlatform.purchaseStream; late StreamSubscription subscription; subscription = stream.listen((purchaseDetailsList) { @@ -198,8 +199,7 @@ void main() { productDetails: AppStoreProductDetails.fromSKProduct(dummyProductWrapper), applicationUserName: 'appName'); - await InAppPurchaseIosPlatform.instance - .buyNonConsumable(purchaseParam: purchaseParam); + await iapIosPlatform.buyNonConsumable(purchaseParam: purchaseParam); List result = await completer.future; expect(result.length, 2); @@ -211,8 +211,7 @@ void main() { () async { List details = []; Completer completer = Completer(); - Stream> stream = - InAppPurchaseIosPlatform.instance.purchaseStream; + Stream> stream = iapIosPlatform.purchaseStream; late StreamSubscription subscription; subscription = stream.listen((purchaseDetailsList) { @@ -226,8 +225,7 @@ void main() { productDetails: AppStoreProductDetails.fromSKProduct(dummyProductWrapper), applicationUserName: 'appName'); - await InAppPurchaseIosPlatform.instance - .buyConsumable(purchaseParam: purchaseParam); + await iapIosPlatform.buyConsumable(purchaseParam: purchaseParam); List result = await completer.future; expect(result.length, 2); @@ -240,8 +238,8 @@ void main() { AppStoreProductDetails.fromSKProduct(dummyProductWrapper), applicationUserName: 'appName'); expect( - () => InAppPurchaseIosPlatform.instance - .buyConsumable(purchaseParam: purchaseParam, autoConsume: false), + () => iapIosPlatform.buyConsumable( + purchaseParam: purchaseParam, autoConsume: false), throwsA(isInstanceOf())); }); @@ -251,8 +249,7 @@ void main() { Completer completer = Completer(); late IAPError error; - Stream> stream = - InAppPurchaseIosPlatform.instance.purchaseStream; + Stream> stream = iapIosPlatform.purchaseStream; late StreamSubscription subscription; subscription = stream.listen((purchaseDetailsList) { details.addAll(purchaseDetailsList); @@ -268,8 +265,7 @@ void main() { productDetails: AppStoreProductDetails.fromSKProduct(dummyProductWrapper), applicationUserName: 'appName'); - await InAppPurchaseIosPlatform.instance - .buyNonConsumable(purchaseParam: purchaseParam); + await iapIosPlatform.buyNonConsumable(purchaseParam: purchaseParam); IAPError completerError = await completer.future; expect(completerError.code, 'purchase_error'); @@ -283,14 +279,13 @@ void main() { test('should complete purchase', () async { List details = []; Completer completer = Completer(); - Stream> stream = - InAppPurchaseIosPlatform.instance.purchaseStream; + Stream> stream = iapIosPlatform.purchaseStream; late StreamSubscription subscription; subscription = stream.listen((purchaseDetailsList) { details.addAll(purchaseDetailsList); purchaseDetailsList.forEach((purchaseDetails) { if (purchaseDetails.pendingCompletePurchase) { - InAppPurchaseIosPlatform.instance.completePurchase(purchaseDetails); + iapIosPlatform.completePurchase(purchaseDetails); completer.complete(details); subscription.cancel(); } @@ -300,8 +295,7 @@ void main() { productDetails: AppStoreProductDetails.fromSKProduct(dummyProductWrapper), applicationUserName: 'appName'); - await InAppPurchaseIosPlatform.instance - .buyNonConsumable(purchaseParam: purchaseParam); + await iapIosPlatform.buyNonConsumable(purchaseParam: purchaseParam); List result = await completer.future; expect(result.length, 2); expect(result.first.productID, dummyProductWrapper.productIdentifier); From 67a491f686461ce4f450a52b18184fa56172bcb3 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 5 May 2021 12:45:58 +0200 Subject: [PATCH 30/33] Added TODO comment to add example --- packages/in_app_purchase/in_app_purchase_ios/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/in_app_purchase/in_app_purchase_ios/README.md b/packages/in_app_purchase/in_app_purchase_ios/README.md index 025ed36b72a6..3ab033849f39 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/README.md +++ b/packages/in_app_purchase/in_app_purchase_ios/README.md @@ -31,4 +31,7 @@ dependencies: ... ``` +## TODO +- [ ] Add an example application demonstrating the use of the [in_app_purchase_ios] package (see also issue [flutter/flutter#81695](https://github.com/flutter/flutter/issues/81695)). + [1]: ../in_app_purchase/in_app_purchase \ No newline at end of file From 3d644b59f49969a426689c78c33c74fe4ff72b94 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 10 May 2021 16:22:02 +0200 Subject: [PATCH 31/33] Added iOS example --- .../in_app_purchase_ios/example/README.md | 75 ++ .../ios/Flutter/AppFrameworkInfo.plist | 26 + .../example/ios/Flutter/Debug.xcconfig | 2 + .../example/ios/Flutter/Release.xcconfig | 2 + .../in_app_purchase_ios/example/ios/Podfile | 45 ++ .../ios/Runner.xcodeproj/project.pbxproj | 664 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/xcschemes/Runner.xcscheme | 97 +++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../example/ios/Runner/AppDelegate.h | 10 + .../example/ios/Runner/AppDelegate.m | 17 + .../AppIcon.appiconset/Contents.json | 122 ++++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 11112 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 564 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 1588 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 1025 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 1716 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 1920 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 1895 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 3831 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 1888 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 3294 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 3612 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + .../example/ios/Runner/Configuration.storekit | 96 +++ .../example/ios/Runner/Info.plist | 45 ++ .../example/ios/Runner/main.m | 13 + .../in_app_purchase_pluginTests/Info.plist | 22 + .../example/lib/consumable_store.dart | 51 ++ .../in_app_purchase_ios/example/lib/main.dart | 409 +++++++++++ .../in_app_purchase_ios/example/pubspec.yaml | 33 + .../test_driver/test/integration_test.dart | 18 + .../ios/Tests/InAppPurchasePluginTest.m | 2 +- .../ios/Tests/PaymentQueueTest.m | 2 +- .../ios/Tests/ProductRequestHandlerTest.m | 2 +- .../in_app_purchase_ios/ios/Tests/Stubs.h | 2 +- .../ios/Tests/TranslatorTest.m | 2 +- ...se.podspec => in_app_purchase_ios.podspec} | 2 +- .../in_app_purchase_ios/pubspec.yaml | 1 + 50 files changed, 1870 insertions(+), 6 deletions(-) create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/README.md create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/AppFrameworkInfo.plist create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/Debug.xcconfig create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/Release.xcconfig create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Podfile create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/AppDelegate.h create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/AppDelegate.m create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Base.lproj/Main.storyboard create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Configuration.storekit create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Info.plist create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/main.m create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/in_app_purchase_pluginTests/Info.plist create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/lib/consumable_store.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/lib/main.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/pubspec.yaml create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/test_driver/test/integration_test.dart rename packages/in_app_purchase/in_app_purchase_ios/ios/{in_app_purchase.podspec => in_app_purchase_ios.podspec} (96%) diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/README.md b/packages/in_app_purchase/in_app_purchase_ios/example/README.md new file mode 100644 index 000000000000..9cf98bf02e79 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/README.md @@ -0,0 +1,75 @@ +# In App Purchase iOS Example + +Demonstrates how to use the In App Purchase iOS (IAP) Plugin. + +## Getting Started + +### Preparation + +There's a significant amount of setup required for testing in app purchases +successfully, including registering new app IDs and store entries to use for +testing in App Store Connect. The App Store requires developers to configure +an app with in-app items for purchase to call their in-app-purchase APIs. +The App Store has extensive documentation on how to do this, and we've also +included a high level guide below. + +* [In-App Purchase (App Store)](https://developer.apple.com/in-app-purchase/) + +### iOS + +When using Xcode 12 and iOS 14 or higher you can run the example in the simulator or on a device without +having to configure an App in App Store Connect. The example app is set up to use StoreKit Testing configured +in the `example/ios/Runner/Configuration.storekit` file (as documented in the article [Setting Up StoreKit Testing in Xcode](https://developer.apple.com/documentation/xcode/setting_up_storekit_testing_in_xcode?language=objc)). +To run the application take the following steps (note that it will only work when running from Xcode): + +1. Open the example app with Xcode, `File > Open File` `example/ios/Runner.xcworkspace`; + +2. Within Xcode edit the current scheme, `Product > Scheme > Edit Scheme...` (or press `Command + Shift + ,`); + +3. Enable StoreKit testing: + a. Select the `Run` action; + b. Click `Options` in the action settings; + c. Select the `Configuration.storekit` for the StoreKit Configuration option. + +4. Click the `Close` button to close the scheme editor; + +5. Select the device you want to run the example App on; + +6. Run the application using `Product > Run` (or hit the run button). + +When testing on pre-iOS 14 you can't run the example app on a simulator and you will need to configure an app in App Store Connect. You can do so by following the steps below: + +1. Follow ["Workflow for configuring in-app + purchases"](https://help.apple.com/app-store-connect/#/devb57be10e7), a + detailed guide on all the steps needed to enable IAPs for an app. Complete + steps 1 ("Sign a Paid Applications Agreement") and 2 ("Configure in-app + purchases"). + + For step #2, "Configure in-app purchases in App Store Connect," you'll want + to create the following products: + + - A consumable with product ID `consumable` + - An upgrade with product ID `upgrade` + - An auto-renewing subscription with product ID `subscription_silver` + - An non-renewing subscription with product ID `subscription_gold` + +2. In XCode, `File > Open File` `example/ios/Runner.xcworkspace`. Update the + Bundle ID to match the Bundle ID of the app created in step #1. + +3. [Create a Sandbox tester + account](https://help.apple.com/app-store-connect/#/dev8b997bee1) to test the + in-app purchases with. + +4. Use `flutter run` to install the app and test it. Note that you need to test + it on a real device instead of a simulator. Next click on one of the products + in the example App, this enables the "SANDBOX ACCOUNT" section in the iOS + settings. You will now be asked to sign in with your sandbox test account to + complete the purchase (no worries you won't be charged). If for some reason + you aren't asked to sign-in or the wrong user is listed, go into the iOS + settings ("Settings" -> "App Store" -> "SANDBOX ACCOUNT") and update your + sandbox account from there. This procedure is explained in great detail in + the [Testing In-App Purchases with Sandbox](https://developer.apple.com/documentation/storekit/in-app_purchase/testing_in-app_purchases_with_sandbox?language=objc) article. + + +**Important:** signing into any production service (including iTunes!) with the +sandbox test account will permanently invalidate it. diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..9367d483e44e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 8.0 + + diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/Debug.xcconfig b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..e8efba114687 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/Release.xcconfig b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..399e9340e6f6 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Podfile b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Podfile new file mode 100644 index 000000000000..7079e94dc672 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Podfile @@ -0,0 +1,45 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + + target 'in_app_purchase_pluginTests' do + inherit! :search_paths + + # Matches in_app_purchase test_spec dependency. + pod 'OCMock','3.5' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..3f2cd3d7e434 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,664 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0FFCF66105590202CD84C7AA /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1630769A874F9381BC761FE1 /* libPods-Runner.a */; }; + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 688DE35121F2A5A100EA2684 /* TranslatorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 688DE35021F2A5A100EA2684 /* TranslatorTest.m */; }; + 6896B34621E9363700D37AEF /* ProductRequestHandlerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6896B34521E9363700D37AEF /* ProductRequestHandlerTest.m */; }; + 6896B34C21EEB4B800D37AEF /* Stubs.m in Sources */ = {isa = PBXBuildFile; fileRef = 6896B34B21EEB4B800D37AEF /* Stubs.m */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + A5279298219369C600FF69E6 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5279297219369C600FF69E6 /* StoreKit.framework */; }; + A59001A721E69658004A3E5E /* InAppPurchasePluginTest.m in Sources */ = {isa = PBXBuildFile; fileRef = A59001A621E69658004A3E5E /* InAppPurchasePluginTest.m */; }; + F78AF3142342BC89008449C7 /* PaymentQueueTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F78AF3132342BC89008449C7 /* PaymentQueueTest.m */; }; + FF1D041E5E26858D1AF300BC /* libPods-in_app_purchase_pluginTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 28CC9057029D80DB8A500E56 /* libPods-in_app_purchase_pluginTests.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + A59001A921E69658004A3E5E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1630769A874F9381BC761FE1 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 1F1978CCF9BBD9FE5606B43A /* Pods-in_app_purchase_pluginTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-in_app_purchase_pluginTests.release.xcconfig"; path = "Target Support Files/Pods-in_app_purchase_pluginTests/Pods-in_app_purchase_pluginTests.release.xcconfig"; sourceTree = ""; }; + 2550EB3A5A3E749A54ADCA2D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 28CC9057029D80DB8A500E56 /* libPods-in_app_purchase_pluginTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-in_app_purchase_pluginTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 67D5CD73380CB78474FA613C /* Pods-in_app_purchase_pluginTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-in_app_purchase_pluginTests.debug.xcconfig"; path = "Target Support Files/Pods-in_app_purchase_pluginTests/Pods-in_app_purchase_pluginTests.debug.xcconfig"; sourceTree = ""; }; + 688DE35021F2A5A100EA2684 /* TranslatorTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = TranslatorTest.m; path = ../../../ios/Tests/TranslatorTest.m; sourceTree = ""; }; + 6896B34521E9363700D37AEF /* ProductRequestHandlerTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = ProductRequestHandlerTest.m; path = ../../../ios/Tests/ProductRequestHandlerTest.m; sourceTree = ""; }; + 6896B34A21EEB4B800D37AEF /* Stubs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = Stubs.h; path = ../../../ios/Tests/Stubs.h; sourceTree = ""; }; + 6896B34B21EEB4B800D37AEF /* Stubs.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = Stubs.m; path = ../../../ios/Tests/Stubs.m; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A5279297219369C600FF69E6 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + A59001A421E69658004A3E5E /* in_app_purchase_pluginTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = in_app_purchase_pluginTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + A59001A621E69658004A3E5E /* InAppPurchasePluginTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = InAppPurchasePluginTest.m; path = ../../../ios/Tests/InAppPurchasePluginTest.m; sourceTree = ""; }; + A59001A821E69658004A3E5E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E4F9651425A612301059769C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + F6E5D5F926131C4800C68BED /* Configuration.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Configuration.storekit; sourceTree = ""; }; + F78AF3132342BC89008449C7 /* PaymentQueueTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PaymentQueueTest.m; path = ../../../ios/Tests/PaymentQueueTest.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A5279298219369C600FF69E6 /* StoreKit.framework in Frameworks */, + 0FFCF66105590202CD84C7AA /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A59001A121E69658004A3E5E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FF1D041E5E26858D1AF300BC /* libPods-in_app_purchase_pluginTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0B4403AC68C3196AECF5EF89 /* Pods */ = { + isa = PBXGroup; + children = ( + E4F9651425A612301059769C /* Pods-Runner.debug.xcconfig */, + 2550EB3A5A3E749A54ADCA2D /* Pods-Runner.release.xcconfig */, + 67D5CD73380CB78474FA613C /* Pods-in_app_purchase_pluginTests.debug.xcconfig */, + 1F1978CCF9BBD9FE5606B43A /* Pods-in_app_purchase_pluginTests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + A59001A521E69658004A3E5E /* in_app_purchase_pluginTests */, + 97C146EF1CF9000F007C117D /* Products */, + E4DB99639FAD8ADED6B572FC /* Frameworks */, + 0B4403AC68C3196AECF5EF89 /* Pods */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + A59001A421E69658004A3E5E /* in_app_purchase_pluginTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + F6E5D5F926131C4800C68BED /* Configuration.storekit */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + A59001A521E69658004A3E5E /* in_app_purchase_pluginTests */ = { + isa = PBXGroup; + children = ( + A59001A621E69658004A3E5E /* InAppPurchasePluginTest.m */, + 6896B34521E9363700D37AEF /* ProductRequestHandlerTest.m */, + F78AF3132342BC89008449C7 /* PaymentQueueTest.m */, + A59001A821E69658004A3E5E /* Info.plist */, + 6896B34A21EEB4B800D37AEF /* Stubs.h */, + 6896B34B21EEB4B800D37AEF /* Stubs.m */, + 688DE35021F2A5A100EA2684 /* TranslatorTest.m */, + ); + path = in_app_purchase_pluginTests; + sourceTree = ""; + }; + E4DB99639FAD8ADED6B572FC /* Frameworks */ = { + isa = PBXGroup; + children = ( + A5279297219369C600FF69E6 /* StoreKit.framework */, + 1630769A874F9381BC761FE1 /* libPods-Runner.a */, + 28CC9057029D80DB8A500E56 /* libPods-in_app_purchase_pluginTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + EDD921296E29F853F7B69716 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; + A59001A321E69658004A3E5E /* in_app_purchase_pluginTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = A59001AD21E69658004A3E5E /* Build configuration list for PBXNativeTarget "in_app_purchase_pluginTests" */; + buildPhases = ( + 321E2F5767F55B0A360AA77E /* [CP] Check Pods Manifest.lock */, + A59001A021E69658004A3E5E /* Sources */, + A59001A121E69658004A3E5E /* Frameworks */, + A59001A221E69658004A3E5E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + A59001AA21E69658004A3E5E /* PBXTargetDependency */, + ); + name = in_app_purchase_pluginTests; + productName = in_app_purchase_pluginTests; + productReference = A59001A421E69658004A3E5E /* in_app_purchase_pluginTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Original; + LastUpgradeCheck = 1100; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + SystemCapabilities = { + com.apple.InAppPurchase = { + enabled = 1; + }; + }; + }; + A59001A321E69658004A3E5E = { + CreatedOnToolsVersion = 10.0; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + A59001A321E69658004A3E5E /* in_app_purchase_pluginTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A59001A221E69658004A3E5E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 321E2F5767F55B0A360AA77E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-in_app_purchase_pluginTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + EDD921296E29F853F7B69716 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A59001A021E69658004A3E5E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F78AF3142342BC89008449C7 /* PaymentQueueTest.m in Sources */, + 6896B34621E9363700D37AEF /* ProductRequestHandlerTest.m in Sources */, + 688DE35121F2A5A100EA2684 /* TranslatorTest.m in Sources */, + A59001A721E69658004A3E5E /* InAppPurchasePluginTest.m in Sources */, + 6896B34C21EEB4B800D37AEF /* Stubs.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + A59001AA21E69658004A3E5E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = A59001A921E69658004A3E5E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.inAppPurchaseExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.inAppPurchaseExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + A59001AB21E69658004A3E5E /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 67D5CD73380CB78474FA613C /* Pods-in_app_purchase_pluginTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = in_app_purchase_pluginTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "sample.changme.in-app-purchase-pluginTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + A59001AC21E69658004A3E5E /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1F1978CCF9BBD9FE5606B43A /* Pods-in_app_purchase_pluginTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = in_app_purchase_pluginTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "sample.changme.in-app-purchase-pluginTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A59001AD21E69658004A3E5E /* Build configuration list for PBXNativeTarget "in_app_purchase_pluginTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A59001AB21E69658004A3E5E /* Debug */, + A59001AC21E69658004A3E5E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..e1fad2d518ae --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/AppDelegate.h b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/AppDelegate.h @@ -0,0 +1,10 @@ +// 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 +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/AppDelegate.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..30b87969f44a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/AppDelegate.m @@ -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. + +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d36b1fab2d9d --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..3d43d11e66f4de3da27ed045ca4fe38ad8b48094 GIT binary patch literal 11112 zcmeHN3sh5A)((b(k1DoWZSj%R+R=^`Y(b;ElB$1^R>iT7q6h&WAVr806i~>Gqn6rM z>3}bMG&oq%DIriqR35=rtEdos5L6z)YC*Xq0U-$_+Il@RaU zXYX%+``hR28`(B*uJ6G9&iz>|)PS%!)9N`7=LcmcxH}k69HPyT-%S zH7+jBCC<%76cg_H-n41cTqnKn`u_V9p~XaTLUe3s{KRPSTeK6apP4Jg%VQ$e#72ms zxyWzmGSRwN?=fRgpx!?W&ZsrLfuhAsRxm%;_|P@3@3~BJwY4ZVBJ3f&$5x>`^fD?d zI+z!v#$!gz%FtL*%mR^Uwa*8LJFZ_;X!y$cD??W#c)31l@ervOa_Qk86R{HJiZb$f z&&&0xYmB{@D@yl~^l5IXtB_ou{xFiYP(Jr<9Ce{jCN z<3Rf2TD%}_N?y>bgWq|{`RKd}n>P4e8Z-D+(fn^4)+|pv$DcR&i+RHNhv$71F*McT zl`phYBlb;wO`b7)*10XF6UXhY9`@UR*6-#(Zp`vyU(__*te6xYtV&N0(zjMtev{tZ zapmGin===teMXjsS0>CYxUy<2izOKOPai0}!B9+6q$s3CF8W{xUwz?A0ADO5&BsiB z{SFt|KehNd-S#eiDq!y&+mW9N_!wH-i~q|oNm=mEzkx}B?Ehe%q$tK8f=QY#*6rH9 zNHHaG(9WBqzP!!TMEktSVuh$i$4A^b25LK}&1*4W?ul*5pZYjL1OZ@X9?3W7Y|T6} z1SXx0Wn-|!A;fZGGlYn9a1Jz5^8)~v#mXhmm>um{QiGG459N}L<&qyD+sy_ixD@AP zW0XV6w#3(JW>TEV}MD=O0O>k5H>p#&|O zD2mGf0Cz7+>l7`NuzGobt;(o@vb9YiOpHN8QJ9Uva|i7R?7nnq;L_iq+ZqPv*oGu! zN@GuJ9fm;yrEFga63m?1qy|5&fd32<%$yP$llh}Udrp>~fb>M>R55I@BsGYhCj8m1 zC=ziFh4@hoytpfrJlr}FsV|C(aV4PZ^8^`G29(+!Bk8APa#PemJqkF zE{IzwPaE)I&r`OxGk*vPErm6sGKaQJ&6FODW$;gAl_4b_j!oH4yE@ zP~Cl4?kp>Ccc~Nm+0kjIb`U0N7}zrQEN5!Ju|}t}LeXi!baZOyhlWha5lq{Ld2rdo zGz7hAJQt<6^cxXTe0xZjmADL85cC&H+~Lt2siIIh{$~+U#&#^{Ub22IA|ea6 z5j12XLc`~dh$$1>3o0Cgvo*ybi$c*z>n=5L&X|>Wy1~eagk;lcEnf^2^2xB=e58Z` z@Rw{1ssK)NRV+2O6c<8qFl%efHE;uy!mq(Xi1P*H2}LMi z3EqWN2U?eW{J$lSFxDJg-=&RH!=6P9!y|S~gmjg)gPKGMxq6r9cNIhW` zS})-obO}Ao_`;=>@fAwU&=|5$J;?~!s4LN2&XiMXEl>zk9M}tVEg#kkIkbKp%Ig2QJ2aCILCM1E=aN*iuz>;q#T_I7aVM=E4$m_#OWLnXQnFUnu?~(X>$@NP zBJ@Zw>@bmErSuW7SR2=6535wh-R`WZ+5dLqwTvw}Ks8~4F#hh0$Qn^l-z=;>D~St( z-1yEjCCgd*z5qXa*bJ7H2Tk54KiX&=Vd}z?%dcc z`N8oeYUKe17&|B5A-++RHh8WQ%;gN{vf%05@jZF%wn1Z_yk#M~Cn(i@MB_mpcbLj5 zR#QAtC`k=tZ*h|){Mjz`7bNL zGWOW=bjQhX@`Vw^xn#cVwn28c2D9vOb0TLLy~-?-%gOyHSeJ9a>P}5OF5$n}k-pvUa*pvLw)KvG~>QjNWS3LY1f*OkFwPZ5qC@+3^Bt=HZbf`alKY#{pn zdY}NEIgo1sd)^TPxVzO{uvU$|Z-jkK0p1x##LexgQ$zx1^bNPOG*u2RmZkIM!zFVz zz|IsP3I?qrlmjGS2w_(azCvGTnf~flqogV@Q%mH{76uLU(>UB zQZ?*ys3BO&TV{Pj_qEa-hkH7mOMe_Bnu3%CXCgu90XNKf$N)PUc3Ei-&~@tT zI^49Lm^+=TrI=h4h=W@jW{GjWd{_kVuSzAL6Pi@HKYYnnNbtcYdIRww+jY$(30=#p8*if(mzbvau z00#}4Qf+gH&ce_&8y3Z@CZV>b%&Zr7xuPSSqOmoaP@arwPrMx^jQBQQi>YvBUdpBn zI``MZ3I3HLqp)@vk^E|~)zw$0$VI_RPsL9u(kqulmS`tnb%4U)hm{)h@bG*jw@Y*#MX;Th1wu3TrO}Srn_+YWYesEgkO1 zv?P8uWB)is;#&=xBBLf+y5e4?%y>_8$1KwkAJ8UcW|0CIz89{LydfJKr^RF=JFPi}MAv|ecbuZ!YcTSxsD$(Pr#W*oytl?@+2 zXBFb32Kf_G3~EgOS7C`8w!tx}DcCT%+#qa76VSbnHo;4(oJ7)}mm?b5V65ir`7Z}s zR2)m15b#E}z_2@rf34wo!M^CnVoi# ze+S(IK({C6u=Sm{1>F~?)8t&fZpOOPcby;I3jO;7^xmLKM(<%i-nyj9mgw9F1Lq4|DZUHZ4)V9&6fQM(ZxbG{h+}(koiTu`SQw6#6q2Yg z-d+1+MRp$zYT2neIR2cKij2!R;C~ooQ3<;^8)_Gch&ZyEtiQwmF0Mb_)6)4lVEBF< zklXS7hvtu30uJR`3OzcqUNOdYsfrKSGkIQAk|4=&#ggxdU4^Y(;)$8}fQ>lTgQdJ{ zzie8+1$3@E;|a`kzuFh9Se}%RHTmBg)h$eH;gttjL_)pO^10?!bNev6{mLMaQpY<< z7M^ZXrg>tw;vU@9H=khbff?@nu)Yw4G% zGxobPTUR2p_ed7Lvx?dkrN^>Cv$Axuwk;Wj{5Z@#$sK@f4{7SHg%2bpcS{(~s;L(mz@9r$cK@m~ef&vf%1@ z@8&@LLO2lQso|bJD6}+_L1*D^}>oqg~$NipL>QlP3 zM#ATSy@ycMkKs5-0X8nFAtMhO_=$DlWR+@EaZ}`YduRD4A2@!at3NYRHmlENea9IF zN*s>mi?zy*Vv+F+&4-o`Wj}P3mLGM*&M(z|;?d82>hQkkY?e-hJ47mWOLCPL*MO04 z3lE(n2RM=IIo;Z?I=sKJ_h=iJHbQ2<}WW0b@I6Qf-{T=Qn#@N0yG5xH&ofEy^mZMPzd22nR`t!Q)VkNgf*VOxE z$XhOunG3ZN#`Ks$Hp~}`OX5vmHP={GYUJ+-g0%PS$*Qi5+-40M47zJ24vK1#? zb$s^%r?+>#lw$mpZaMa1aO%wlPm3~cno_(S%U&-R;6eK(@`CjswAW2)HfZ>ptItaZ|XqQ z&sHVVL>WCe|E4iPb2~gS5ITs6xfg(kmt&3$YcI=zTuqj37t|+9ojCr(G^ul#p{>k) zM94pI>~5VZ$!*Qurq<@RIXgP3sx-2kL$1Q~da%rnNIh?)&+c~*&e~CYPDhPYjb+Xu zKg5w^XB3(_9{Waa4E(-J-Kq_u6t_k?a8kEHqai-N-4#`SRerO!h}!cS%SMC<)tGix zOzVP^_t!HN&HIPL-ZpcgWitHM&yFRC7!k4zSI+-<_uQ}|tX)n{Ib;X>Xx>i_d*KkH zCzogKQFpP1408_2!ofU|iBq2R8hW6G zuqJs9Tyw{u%-uWczPLkM!MfKfflt+NK9Vk8E!C>AsJwNDRoe2~cL+UvqNP|5J8t)( z0$iMa!jhudJ+fqFn+um&@Oj6qXJd_3-l`S^I1#0fnt!z3?D*hAHr*u(*wR@`4O z#avrtg%s`Fh{?$FtBFM^$@@hW!8ZfF4;=n0<8In&X}-Rp=cd0TqT_ne46$j^r}FzE z26vX^!PzScuQfFfl1HEZ{zL?G88mcc76zHGizWiykBf4m83Z${So-+dZ~YGhm*RO7 zB1gdIdqnFi?qw+lPRFW5?}CQ3Me3G^muvll&4iN+*5#_mmIu;loULMwb4lu9U*dFM z-Sr**(0Ei~u=$3<6>C-G6z4_LNCx||6YtjS)<;hf)YJTPKXW+w%hhCTUAInIse9>r zl2YU6nRb$u-FJlWN*{{%sm_gi_UP5{=?5}5^D2vPzM=oPfNw~azZQ#P zl5z8RtSSiTIpEohC15i-Q1Bk{3&ElsD0uGAOxvbk29VUDmmA0w;^v`W#0`};O3DVE z&+-ca*`YcN%z*#VXWK9Qa-OEME#fykF%|7o=1Y+eF;Rtv0W4~kKRDx9YBHOWhC%^I z$Jec0cC7o37}Xt}cu)NH5R}NT+=2Nap*`^%O)vz?+{PV<2~qX%TzdJOGeKj5_QjqR&a3*K@= P-1+_A+?hGkL;m(J7kc&K literal 0 HcmV?d00001 diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..28c6bf03016f6c994b70f38d1b7346e5831b531f GIT binary patch literal 564 zcmV-40?Yl0P)Px$?ny*JR5%f>l)FnDQ543{x%ZCiu33$Wg!pQFfT_}?5Q|_VSlIbLC`dpoMXL}9 zHfd9&47Mo(7D231gb+kjFxZHS4-m~7WurTH&doVX2KI5sU4v(sJ1@T9eCIKPjsqSr z)C01LsCxk=72-vXmX}CQD#BD;Cthymh&~=f$Q8nn0J<}ZrusBy4PvRNE}+1ceuj8u z0mW5k8fmgeLnTbWHGwfKA3@PdZxhn|PypR&^p?weGftrtCbjF#+zk_5BJh7;0`#Wr zgDpM_;Ax{jO##IrT`Oz;MvfwGfV$zD#c2xckpcXC6oou4ML~ezCc2EtnsQTB4tWNg z?4bkf;hG7IMfhgNI(FV5Gs4|*GyMTIY0$B=_*mso9Ityq$m^S>15>-?0(zQ<8Qy<_TjHE33(?_M8oaM zyc;NxzRVK@DL6RJnX%U^xW0Gpg(lXp(!uK1v0YgHjs^ZXSQ|m#lV7ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 literal 0 HcmV?d00001 diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..f091b6b0bca859a3f474b03065bef75ba58a9e4c GIT binary patch literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ literal 0 HcmV?d00001 diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d0ef06e7edb86cdfe0d15b4b0d98334a86163658 GIT binary patch literal 1716 zcmds$`#;kQ7{|XelZftyR5~xW7?MLxS4^|Hw3&P7^y)@A9Fj{Xm1~_CIV^XZ%SLBn zA;!r`GqGHg=7>xrB{?psZQs88ZaedDoagm^KF{a*>G|dJWRSe^I$DNW008I^+;Kjt z>9p3GNR^I;v>5_`+91i(*G;u5|L+Bu6M=(afLjtkya#yZ175|z$pU~>2#^Z_pCZ7o z1c6UNcv2B3?; zX%qdxCXQpdKRz=#b*q0P%b&o)5ZrNZt7$fiETSK_VaY=mb4GK`#~0K#~9^ zcY!`#Af+4h?UMR-gMKOmpuYeN5P*RKF!(tb`)oe0j2BH1l?=>y#S5pMqkx6i{*=V9JF%>N8`ewGhRE(|WohnD59R^$_36{4>S zDFlPC5|k?;SPsDo87!B{6*7eqmMdU|QZ84>6)Kd9wNfh90=y=TFQay-0__>=<4pk& zYDjgIhL-jQ9o>z32K)BgAH+HxamL{ZL~ozu)Qqe@a`FpH=oQRA8=L-m-1dam(Ix2V z?du;LdMO+ooBelr^_y4{|44tmgH^2hSzPFd;U^!1p>6d|o)(-01z{i&Kj@)z-yfWQ)V#3Uo!_U}q3u`(fOs`_f^ueFii1xBNUB z6MecwJN$CqV&vhc+)b(p4NzGGEgwWNs z@*lUV6LaduZH)4_g!cE<2G6#+hJrWd5(|p1Z;YJ7ifVHv+n49btR}dq?HHDjl{m$T z!jLZcGkb&XS2OG~u%&R$(X+Z`CWec%QKt>NGYvd5g20)PU(dOn^7%@6kQb}C(%=vr z{?RP(z~C9DPnL{q^@pVw@|Vx~@3v!9dCaBtbh2EdtoNHm4kGxp>i#ct)7p|$QJs+U z-a3qtcPvhihub?wnJqEt>zC@)2suY?%-96cYCm$Q8R%-8$PZYsx3~QOLMDf(piXMm zB=<63yQk1AdOz#-qsEDX>>c)EES%$owHKue;?B3)8aRd}m~_)>SL3h2(9X;|+2#7X z+#2)NpD%qJvCQ0a-uzZLmz*ms+l*N}w)3LRQ*6>|Ub-fyptY(keUxw+)jfwF5K{L9 z|Cl_w=`!l_o><384d&?)$6Nh(GAm=4p_;{qVn#hI8lqewW7~wUlyBM-4Z|)cZr?Rh z=xZ&Ol>4(CU85ea(CZ^aO@2N18K>ftl8>2MqetAR53_JA>Fal`^)1Y--Am~UDa4th zKfCYpcXky$XSFDWBMIl(q=Mxj$iMBX=|j9P)^fDmF(5(5$|?Cx}DKEJa&XZP%OyE`*GvvYQ4PV&!g2|L^Q z?YG}tx;sY@GzMmsY`7r$P+F_YLz)(e}% zyakqFB<6|x9R#TdoP{R$>o7y(-`$$p0NxJ6?2B8tH)4^yF(WhqGZlM3=9Ibs$%U1w zWzcss*_c0=v_+^bfb`kBFsI`d;ElwiU%frgRB%qBjn@!0U2zZehBn|{%uNIKBA7n= zzE`nnwTP85{g;8AkYxA68>#muXa!G>xH22D1I*SiD~7C?7Za+9y7j1SHiuSkKK*^O zsZ==KO(Ua#?YUpXl{ViynyT#Hzk=}5X$e04O@fsMQjb}EMuPWFO0e&8(2N(29$@Vd zn1h8Yd>6z(*p^E{c(L0Lg=wVdupg!z@WG;E0k|4a%s7Up5C0c)55XVK*|x9RQeZ1J@1v9MX;>n34(i>=YE@Iur`0Vah(inE3VUFZNqf~tSz{1fz3Fsn_x4F>o(Yo;kpqvBe-sbwH(*Y zu$JOl0b83zu$JMvy<#oH^Wl>aWL*?aDwnS0iEAwC?DK@aT)GHRLhnz2WCvf3Ba;o=aY7 z2{Asu5MEjGOY4O#Ggz@@J;q*0`kd2n8I3BeNuMmYZf{}pg=jTdTCrIIYuW~luKecn z+E-pHY%ohj@uS0%^ z&(OxwPFPD$+#~`H?fMvi9geVLci(`K?Kj|w{rZ9JgthFHV+=6vMbK~0)Ea<&WY-NC zy-PnZft_k2tfeQ*SuC=nUj4H%SQ&Y$gbH4#2sT0cU0SdFs=*W*4hKGpuR1{)mV;Qf5pw4? zfiQgy0w3fC*w&Bj#{&=7033qFR*<*61B4f9K%CQvxEn&bsWJ{&winp;FP!KBj=(P6 z4Z_n4L7cS;ao2)ax?Tm|I1pH|uLpDSRVghkA_UtFFuZ0b2#>!8;>-_0ELjQSD-DRd z4im;599VHDZYtnWZGAB25W-e(2VrzEh|etsv2YoP#VbIZ{aFkwPrzJ#JvCvA*mXS& z`}Q^v9(W4GiSs}#s7BaN!WA2bniM$0J(#;MR>uIJ^uvgD3GS^%*ikdW6-!VFUU?JV zZc2)4cMsX@j z5HQ^e3BUzOdm}yC-xA%SY``k$rbfk z;CHqifhU*jfGM@DkYCecD9vl*qr58l6x<8URB=&%{!Cu3RO*MrKZ4VO}V6R0a zZw3Eg^0iKWM1dcTYZ0>N899=r6?+adUiBKPciJw}L$=1f4cs^bio&cr9baLF>6#BM z(F}EXe-`F=f_@`A7+Q&|QaZ??Txp_dB#lg!NH=t3$G8&06MFhwR=Iu*Im0s_b2B@| znW>X}sy~m#EW)&6E&!*0%}8UAS)wjt+A(io#wGI@Z2S+Ms1Cxl%YVE800007ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 literal 0 HcmV?d00001 diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c8f9ed8f5cee1c98386d13b17e89f719e83555b2 GIT binary patch literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 literal 0 HcmV?d00001 diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..75b2d164a5a98e212cca15ea7bf2ab5de5108680 GIT binary patch literal 3831 zcmVjJBgitF5mAp-i>4+KS_oR{|13AP->1TD4=w)g|)JHOx|a2Wk1Va z!k)vP$UcQ#mdj%wNQoaJ!w>jv_6&JPyutpQps?s5dmDQ>`%?Bvj>o<%kYG!YW6H-z zu`g$@mp`;qDR!51QaS}|ZToSuAGcJ7$2HF0z`ln4t!#Yg46>;vGG9N9{V@9z#}6v* zfP?}r6b{*-C*)(S>NECI_E~{QYzN5SXRmVnP<=gzP+_Sp(Aza_hKlZ{C1D&l*(7IKXxQC1Z9#6wx}YrGcn~g%;icdw>T0Rf^w0{ z$_wn1J+C0@!jCV<%Go5LA45e{5gY9PvZp8uM$=1}XDI+9m7!A95L>q>>oe0$nC->i zeexUIvq%Uk<-$>DiDb?!In)lAmtuMWxvWlk`2>4lNuhSsjAf2*2tjT`y;@d}($o)S zn(+W&hJ1p0xy@oxP%AM15->wPLp{H!k)BdBD$toBpJh+crWdsNV)qsHaqLg2_s|Ih z`8E9z{E3sA!}5aKu?T!#enD(wLw?IT?k-yWVHZ8Akz4k5(TZJN^zZgm&zM28sfTD2BYJ|Fde3Xzh;;S` z=GXTnY4Xc)8nYoz6&vF;P7{xRF-{|2Xs5>a5)@BrnQ}I(_x7Cgpx#5&Td^4Q9_FnQ zX5so*;#8-J8#c$OlA&JyPp$LKUhC~-e~Ij!L%uSMu!-VZG7Hx-L{m2DVR2i=GR(_% zCVD!4N`I)&Q5S`?P&fQZ=4#Dgt_v2-DzkT}K(9gF0L(owe-Id$Rc2qZVLqI_M_DyO z9@LC#U28_LU{;wGZ&))}0R2P4MhajKCd^K#D+JJ&JIXZ_p#@+7J9A&P<0kdRujtQ_ zOy>3=C$kgi6$0pW06KaLz!21oOryKM3ZUOWqppndxfH}QpgjEJ`j7Tzn5bk6K&@RA?vl##y z$?V~1E(!wB5rH`>3nc&@)|#<1dN2cMzzm=PGhQ|Yppne(C-Vlt450IXc`J4R0W@I7 zd1e5uW6juvO%ni(WX7BsKx3MLngO7rHO;^R5I~0^nE^9^E_eYLgiR9&KnJ)pBbfno zSVnW$0R+&6jOOsZ82}nJ126+c|%svPo;TeUku<2G7%?$oft zyaO;tVo}(W)VsTUhq^XmFi#2z%-W9a{7mXn{uzivYQ_d6b7VJG{77naW(vHt-uhnY zVN#d!JTqVh(7r-lhtXVU6o})aZbDt_;&wJVGl2FKYFBFpU-#9U)z#(A%=IVnqytR$SY-sO( z($oNE09{D^@OuYPz&w~?9>Fl5`g9u&ecFGhqX=^#fmR=we0CJw+5xna*@oHnkahk+ z9aWeE3v|An+O5%?4fA&$Fgu~H_YmqR!yIU!bFCk4!#pAj%(lI(A5n)n@Id#M)O9Yx zJU9oKy{sRAIV3=5>(s8n{8ryJ!;ho}%pn6hZKTKbqk=&m=f*UnK$zW3YQP*)pw$O* zIfLA^!-bmBl6%d_n$#tP8Zd_(XdA*z*WH|E_yILwjtI~;jK#v-6jMl^?<%Y%`gvpwv&cFb$||^v4D&V=aNy?NGo620jL3VZnA%s zH~I|qPzB~e(;p;b^gJr7Ure#7?8%F0m4vzzPy^^(q4q1OdthF}Fi*RmVZN1OwTsAP zn9CZP`FazX3^kG(KodIZ=Kty8DLTy--UKfa1$6XugS zk%6v$Kmxt6U!YMx0JQ)0qX*{CXwZZk$vEROidEc7=J-1;peNat!vS<3P-FT5po>iE z!l3R+<`#x|+_hw!HjQGV=8!q|76y8L7N8gP3$%0kfush|u0uU^?dKBaeRSBUpOZ0c z62;D&Mdn2}N}xHRFTRI?zRv=>=AjHgH}`2k4WK=#AHB)UFrR-J87GgX*x5fL^W2#d z=(%K8-oZfMO=i{aWRDg=FX}UubM4eotRDcn;OR#{3q=*?3mE3_oJ-~prjhxh%PgQT zyn)Qozaq0@o&|LEgS{Ind4Swsr;b`u185hZPOBLL<`d2%^Yp1?oL)=jnLi;Zo0ZDliTtQ^b5SmfIMe{T==zZkbvn$KTQGlbG8w}s@M3TZnde;1Am46P3juKb zl9GU&3F=q`>j!`?SyH#r@O59%@aMX^rx}Nxe<>NqpUp5=lX1ojGDIR*-D^SDuvCKF z?3$xG(gVUsBERef_YjPFl^rU9EtD{pt z0CXwpN7BN3!8>hajGaTVk-wl=9rxmfWtIhC{mheHgStLi^+Nz12a?4r(fz)?3A%at zMlvQmL<2-R)-@G1wJ0^zQK%mR=r4d{Y3fHp){nWXUL#|CqXl(+v+qDh>FkF9`eWrW zfr^D%LNfOcTNvtx0JXR35J0~Jpi2#P3Q&80w+nqNfc}&G0A~*)lGHKv=^FE+b(37|)zL;KLF>oiGfb(?&1 zV3XRu!Sw>@quKiab%g6jun#oZ%!>V#A%+lNc?q>6+VvyAn=kf_6z^(TZUa4Eelh{{ zqFX-#dY(EV@7l$NE&kv9u9BR8&Ojd#ZGJ6l8_BW}^r?DIS_rU2(XaGOK z225E@kH5Opf+CgD^{y29jD4gHbGf{1MD6ggQ&%>UG4WyPh5q_tb`{@_34B?xfSO*| zZv8!)q;^o-bz`MuxXk*G^}(6)ACb@=Lfs`Hxoh>`Y0NE8QRQ!*p|SH@{r8=%RKd4p z+#Ty^-0kb=-H-O`nAA3_6>2z(D=~Tbs(n8LHxD0`R0_ATFqp-SdY3(bZ3;VUM?J=O zKCNsxsgt@|&nKMC=*+ZqmLHhX1KHbAJs{nGVMs6~TiF%Q)P@>!koa$%oS zjXa=!5>P`vC-a}ln!uH1ooeI&v?=?v7?1n~P(wZ~0>xWxd_Aw;+}9#eULM7M8&E?Y zC-ZLhi3RoM92SXUb-5i-Lmt5_rfjE{6y^+24`y$1lywLyHO!)Boa7438K4#iLe?rh z2O~YGSgFUBH?og*6=r9rme=peP~ah`(8Zt7V)j5!V0KPFf_mebo3z95U8(up$-+EA^9dTRLq>Yl)YMBuch9%=e5B`Vnb>o zt03=kq;k2TgGe4|lGne&zJa~h(UGutjP_zr?a7~#b)@15XNA>Dj(m=gg2Q5V4-$)D|Q9}R#002ovPDHLkV1o7DH3k3x literal 0 HcmV?d00001 diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..c4df70d39da7941ef3f6dcb7f06a192d8dcb308d GIT binary patch literal 1888 zcmV-m2cP(fP)x~L`~4d)Rspd&<9kFh{hn*KP1LP0~$;u(LfAu zp%fx&qLBcRHx$G|3q(bv@+b;o0*D|jwD-Q9uQR(l*ST}s+uPgQ-MeFwZ#GS?b332? z&Tk$&_miXn3IGq)AmQ)3sisq{raD4(k*bHvpCe-TdWq^NRTEVM)i9xbgQ&ccnUVx* zEY%vS%gDcSg=!tuIK8$Th2_((_h^+7;R|G{n06&O2#6%LK`a}n?h_fL18btz<@lFG za}xS}u?#DBMB> zw^b($1Z)`9G?eP95EKi&$eOy@K%h;ryrR3la%;>|o*>CgB(s>dDcNOXg}CK9SPmD? zmr-s{0wRmxUnbDrYfRvnZ@d z6johZ2sMX{YkGSKWd}m|@V7`Degt-43=2M?+jR%8{(H$&MLLmS;-|JxnX2pnz;el1jsvqQz}pGSF<`mqEXRQ5sC4#BbwnB_4` zc5bFE-Gb#JV3tox9fp-vVEN{(tOCpRse`S+@)?%pz+zVJXSooTrNCUg`R6`hxwb{) zC@{O6MKY8tfZ5@!yy=p5Y|#+myRL=^{tc(6YgAnkg3I(Cd!r5l;|;l-MQ8B`;*SCE z{u)uP^C$lOPM z5d~UhKhRRmvv{LIa^|oavk1$QiEApSrP@~Jjbg`<*dW4TO?4qG%a%sTPUFz(QtW5( zM)lA+5)0TvH~aBaOAs|}?u2FO;yc-CZ1gNM1dAxJ?%m?YsGR`}-xk2*dxC}r5j$d* zE!#Vtbo69h>V4V`BL%_&$} z+oJAo@jQ^Tk`;%xw-4G>hhb&)B?##U+(6Fi7nno`C<|#PVA%$Y{}N-?(Gc$1%tr4Pc}}hm~yY#fTOe!@v9s-ik$dX~|ygArPhByaXn8 zpI^FUjNWMsTFKTP3X7m?UK)3m zp6rI^_zxRYrx6_QmhoWoDR`fp4R7gu6;gdO)!KexaoO2D88F9x#TM1(9Bn7g;|?|o z)~$n&Lh#hCP6_LOPD>a)NmhW})LADx2kq=X7}7wYRj-0?dXr&bHaRWCfSqvzFa=sn z-8^gSyn-RmH=BZ{AJZ~!8n5621GbUJV7Qvs%JNv&$%Q17s_X%s-41vAPfIR>;x0Wlqr5?09S>x#%Qkt>?(&XjFRY}*L6BeQ3 z<6XEBh^S7>AbwGm@XP{RkeEKj6@_o%oV?hDuUpUJ+r#JZO?!IUc;r0R?>mi)*ZpQ) z#((dn=A#i_&EQn|hd)N$#A*fjBFuiHcYvo?@y1 z5|fV=a^a~d!c-%ZbMNqkMKiSzM{Yq=7_c&1H!mXk60Uv32dV;vMg&-kQ)Q{+PFtwc zj|-uQ;b^gts??J*9VxxOro}W~Q9j4Em|zSRv)(WSO9$F$s=Ydu%Q+5DOid~lwk&we zY%W(Z@ofdwPHncEZzZgmqS|!gTj3wQq9rxQy+^eNYKr1mj&?tm@wkO*9@UtnRMG>c aR{jt9+;fr}hV%pg00001^@s67{VYS000c7NklQEG_j zup^)eW&WUIApqy$=APz8jE@awGp)!bsTjDbrJO`$x^ZR^dr;>)LW>{ zs70vpsD38v)19rI=GNk1b(0?Js9~rjsQsu*K;@SD40RB-3^gKU-MYC7G!Bw{fZsqp zih4iIi;Hr_xZ033Iu{sQxLS=}yBXgLMn40d++>aQ0#%8D1EbGZp7+ z5=mK?t31BkVYbGOxE9`i748x`YgCMwL$qMsChbSGSE1`p{nSmadR zcQ#R)(?!~dmtD0+D2!K zR9%!Xp1oOJzm(vbLvT^$IKp@+W2=-}qTzTgVtQ!#Y7Gxz}stUIm<1;oBQ^Sh2X{F4ibaOOx;5ZGSNK z0maF^@(UtV$=p6DXLgRURwF95C=|U8?osGhgOED*b z7woJ_PWXBD>V-NjQAm{~T%sjyJ{5tn2f{G%?J!KRSrrGvQ1(^`YLA5B!~eycY(e5_ z*%aa{at13SxC(=7JT7$IQF~R3sy`Nn%EMv!$-8ZEAryB*yB1k&stni)=)8-ODo41g zkJu~roIgAih94tb=YsL%iH5@^b~kU9M-=aqgXIrbtxMpFy5mekFm#edF9z7RQ6V}R zBIhbXs~pMzt0VWy1Fi$^fh+1xxLDoK09&5&MJl(q#THjPm(0=z2H2Yfm^a&E)V+a5 zbi>08u;bJsDRUKR9(INSc7XyuWv(JsD+BB*0hS)FO&l&7MdViuur@-<-EHw>kHRGY zqoT}3fDv2-m{NhBG8X}+rgOEZ;amh*DqN?jEfQdqxdj08`Sr=C-KmT)qU1 z+9Cl)a1mgXxhQiHVB}l`m;-RpmKy?0*|yl?FXvJkFxuu!fKlcmz$kN(a}i*saM3nr z0!;a~_%Xqy24IxA2rz<+08=B-Q|2PT)O4;EaxP^6qixOv7-cRh?*T?zZU`{nIM-at zTKYWr9rJ=tppQ9I#Z#mLgINVB!pO-^FOcvFw6NhV0gztuO?g ztoA*C-52Q-Z-P#xB4HAY3KQVd%dz1S4PA3vHp0aa=zAO?FCt zC_GaTyVBg2F!bBr3U@Zy2iJgIAt>1sf$JWA9kh{;L+P*HfUBX1Zy{4MgNbDfBV_ly z!y#+753arsZUt@366jIC0klaC@ckuk!qu=pAyf7&QmiBUT^L1&tOHzsK)4n|pmrVT zs2($4=?s~VejTFHbFdDOwG;_58LkIj1Fh@{glkO#F1>a==ymJS$z;gdedT1zPx4Kj ztjS`y_C}%af-RtpehdQDt3a<=W5C4$)9W@QAse;WUry$WYmr51ml9lkeunUrE`-3e zmq1SgSOPNEE-Mf+AGJ$g0M;3@w!$Ej;hMh=v=I+Lpz^n%Pg^MgwyqOkNyu2c^of)C z1~ALor3}}+RiF*K4+4{(1%1j3pif1>sv0r^mTZ?5Jd-It!tfPfiG_p$AY*Vfak%FG z4z#;wLtw&E&?}w+eKG^=#jF7HQzr8rV0mY<1YAJ_uGz~$E13p?F^fPSzXSn$8UcI$ z8er9{5w5iv0qf8%70zV71T1IBB1N}R5Kp%NO0=5wJalZt8;xYp;b{1K) zHY>2wW-`Sl{=NpR%iu3(u6l&)rc%%cSA#aV7WCowfbFR4wcc{LQZv~o1u_`}EJA3>ki`?9CKYTA!rhO)if*zRdd}Kn zEPfYbhoVE~!FI_2YbC5qAj1kq;xP6%J8+?2PAs?`V3}nyFVD#sV3+uP`pi}{$l9U^ zSz}_M9f7RgnnRhaoIJgT8us!1aB&4!*vYF07Hp&}L zCRlop0oK4DL@ISz{2_BPlezc;xj2|I z23RlDNpi9LgTG_#(w%cMaS)%N`e>~1&a3<{Xy}>?WbF>OOLuO+j&hc^YohQ$4F&ze z+hwnro1puQjnKm;vFG~o>`kCeUIlkA-2tI?WBKCFLMBY=J{hpSsQ=PDtU$=duS_hq zHpymHt^uuV1q@uc4bFb{MdG*|VoW@15Osrqt2@8ll0qO=j*uOXn{M0UJX#SUztui9FN4)K3{9!y8PC-AHHvpVTU;x|-7P+taAtyglk#rjlH2 z5Gq8ik}BPaGiM{#Woyg;*&N9R2{J0V+WGB69cEtH7F?U~Kbi6ksi*`CFXsi931q7Y zGO82?whBhN%w1iDetv%~wM*Y;E^)@Vl?VDj-f*RX>{;o_=$fU!&KAXbuadYZ46Zbg z&6jMF=49$uL^73y;;N5jaHYv)BTyfh&`qVLYn?`o6BCA_z-0niZz=qPG!vonK3MW_ zo$V96zM!+kJRs{P-5-rQVse0VBH*n6A58)4uc&gfHMa{gIhV2fGf{st>E8sKyP-$8zp~wJX^A*@DI&-;8>gANXZj zU)R+Y)PB?=)a|Kj>8NXEu^S_h^7R`~Q&7*Kn!xyvzVv&^>?^iu;S~R2e-2fJx-oUb cX)(b1KSk$MOV07*qoM6N<$f&6$jw%VRuvdN2+38CZWny1cRtlsl+0_KtW)EU14Ei(F!UtWuj4IK+3{sK@>rh zs1Z;=(DD&U6+tlyL?UnHVN^&g6QhFi2#HS+*qz;(>63G(`|jRtW|nz$Pv7qTovP!^ zP_jES{mr@O-02w%!^a?^1ZP!_KmQiz0L~jZ=W@Qt`8wzOoclQsAS<5YdH;a(4bGLE zk8s}1If(PSIgVi!XE!5kA?~z*sobvNyohr;=Q_@h2@$6Flyej3J)D-6YfheRGl`HEcPk|~huT_2-U?PfL=4BPV)f1o!%rQ!NMt_MYw-5bUSwQ9Z&zC>u zOrl~UJglJNa%f50Ok}?WB{on`Ci`p^Y!xBA?m@rcJXLxtrE0FhRF3d*ir>yzO|BD$ z3V}HpFcCh6bTzY}Nt_(W%QYd3NG)jJ4<`F<1Od) zfQblTdC&h2lCz`>y?>|9o2CdvC8qZeIZt%jN;B7Hdn2l*k4M4MFEtq`q_#5?}c$b$pf_3y{Y!cRDafZBEj-*OD|gz#PBDeu3QoueOesLzB+O zxjf2wvf6Wwz>@AiOo2mO4=TkAV+g~%_n&R;)l#!cBxjuoD$aS-`IIJv7cdX%2{WT7 zOm%5rs(wqyPE^k5SIpUZ!&Lq4<~%{*>_Hu$2|~Xa;iX*tz8~G6O3uFOS?+)tWtdi| zV2b#;zRN!m@H&jd=!$7YY6_}|=!IU@=SjvGDFtL;aCtw06U;-v^0%k0FOyESt z1Wv$={b_H&8FiRV?MrzoHWd>%v6KTRU;-v^Miiz+@q`(BoT!+<37CKhoKb)|8!+RG z6BQFU^@fRW;s8!mOf2QViKQGk0TVER6EG1`#;Nm39Do^PoT!+<37AD!%oJe86(=et zZ~|sLzU>V-qYiU6V8$0GmU7_K8|Fd0B?+9Un1BhKAz#V~Fk^`mJtlCX#{^8^M8!me z8Yg;8-~>!e<-iG;h*0B1kBKm}hItVGY6WnjVpgnTTAC$rqQ^v)4KvOtpY|sIj@WYg zyw##ZZ5AC2IKNC;^hwg9BPk0wLStlmBr;E|$5GoAo$&Ui_;S9WY62n3)i49|T%C#i017z3J=$RF|KyZWnci*@lW4 z=AKhNN6+m`Q!V3Ye68|8y@%=am>YD0nG99M)NWc20%)gwO!96j7muR}Fr&54SxKP2 zP30S~lt=a*qDlbu3+Av57=9v&vr<6g0&`!8E2fq>I|EJGKs}t|{h7+KT@)LfIV-3K zK)r_fr2?}FFyn*MYoLC>oV-J~eavL2ho4a4^r{E-8m2hi>~hA?_vIG4a*KT;2eyl1 zh_hUvUJpNCFwBvRq5BI*srSle>c6%n`#VNsyC|MGa{(P&08p=C9+WUw9Hl<1o9T4M zdD=_C0F7#o8A_bRR?sFNmU0R6tW`ElnF8p53IdHo#S9(JoZCz}fHwJ6F<&?qrpVqE zte|m%89JQD+XwaPU#%#lVs-@-OL);|MdfINd6!XwP2h(eyafTUsoRkA%&@fe?9m@jw-v(yTTiV2(*fthQH9}SqmsRPVnwwbV$1E(_lkmo&S zF-truCU914_$jpqjr(>Ha4HkM4YMT>m~NosUu&UZ>zirfHo%N6PPs9^_o$WqPA0#5 z%tG>qFCL+b*0s?sZ;Sht0nE7Kl>OVXy=gjWxxK;OJ3yGd7-pZf7JYNcZo2*1SF`u6 zHJyRRxGw9mDlOiXqVMsNe#WX`fC`vrtjSQ%KmLcl(lC>ZOQzG^%iql2w-f_K@r?OE zwCICifM#L-HJyc7Gm>Ern?+Sk3&|Khmu4(~3qa$(m6Ub^U0E5RHq49za|XklN#?kP zl;EstdW?(_4D>kwjWy2f!LM)y?F94kyU3`W!6+AyId-89v}sXJpuic^NLL7GJItl~ zsiuB98AI-(#Mnm|=A-R6&2fwJ0JVSY#Q>&3$zFh|@;#%0qeF=j5Ajq@4i0tIIW z&}sk$&fGwoJpe&u-JeGLi^r?dO`m=y(QO{@h zQqAC7$rvz&5+mo3IqE?h=a~6m>%r5Quapvzq;{y~p zJpyXOBgD9VrW7@#p6l7O?o3feml(DtSL>D^R) zZUY%T2b0-vBAFN7VB;M88!~HuOXi4KcI6aRQ&h|XQ0A?m%j2=l1f0cGP}h(oVfJ`N zz#PpmFC*ieab)zJK<4?^k=g%OjPnkANzbAbmGZHoVRk*mTfm75s_cWVa`l*f$B@xu z5E*?&@seIo#*Y~1rBm!7sF9~~u6Wrj5oICUOuz}CS)jdNIznfzCA(stJ(7$c^e5wN z?lt>eYgbA!kvAR7zYSD&*r1$b|(@;9dcZ^67R0 zXAXJKa|5Sdmj!g578Nwt6d$sXuc&MWezA0Whd`94$h{{?1IwXP4)Tx4obDK%xoFZ_Z zjjHJ_P@R_e5blG@yEjnaJb`l;s%Lb2&=8$&Ct-fV`E^4CUs)=jTk!I}2d&n!f@)bm z@ z_4Dc86+3l2*p|~;o-Sb~oXb_RuLmoifDU^&Te$*FevycC0*nE3Xws8gsWp|Rj2>SM zns)qcYj?^2sd8?N!_w~4v+f-HCF|a$TNZDoNl$I1Uq87euoNgKb6&r26TNrfkUa@o zfdiFA@p{K&mH3b8i!lcoz)V{n8Q@g(vR4ns4r6w;K z>1~ecQR0-<^J|Ndg5fvVUM9g;lbu-){#ghGw(fg>L zh)T5Ljb%lWE;V9L!;Cqk>AV1(rULYF07ZBJbGb9qbSoLAd;in9{)95YqX$J43-dY7YU*k~vrM25 zxh5_IqO0LYZW%oxQ5HOzmk4x{atE*vipUk}sh88$b2tn?!ujEHn`tQLe&vo}nMb&{ zio`xzZ&GG6&ZyN3jnaQy#iVqXE9VT(3tWY$n-)uWDQ|tc{`?fq2F`oQ{;d3aWPg4Hp-(iE{ry>MIPWL> iW8Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000000..89c2725b70f1 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..f2e259c7c939 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Base.lproj/Main.storyboard b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Configuration.storekit b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Configuration.storekit new file mode 100644 index 000000000000..4958a846e67d --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Configuration.storekit @@ -0,0 +1,96 @@ +{ + "products" : [ + { + "displayPrice" : "0.99", + "familyShareable" : false, + "internalID" : "AE10D05D", + "localizations" : [ + { + "description" : "A consumable product.", + "displayName" : "Consumable", + "locale" : "en_US" + } + ], + "productID" : "consumable", + "referenceName" : "consumable", + "type" : "Consumable" + }, + { + "displayPrice" : "10.99", + "familyShareable" : false, + "internalID" : "FABCF067", + "localizations" : [ + { + "description" : "An non-consumable product.", + "displayName" : "Upgrade", + "locale" : "en_US" + } + ], + "productID" : "upgrade", + "referenceName" : "upgrade", + "type" : "NonConsumable" + } + ], + "settings" : { + + }, + "subscriptionGroups" : [ + { + "id" : "D0FEE8D8", + "localizations" : [ + + ], + "name" : "Example Subscriptions", + "subscriptions" : [ + { + "adHocOffers" : [ + + ], + "displayPrice" : "3.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "922EB597", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "A lower level subscription.", + "displayName" : "Subscription Silver", + "locale" : "en_US" + } + ], + "productID" : "subscription_silver", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "subscription_silver", + "subscriptionGroupID" : "D0FEE8D8", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "displayPrice" : "5.99", + "familyShareable" : false, + "groupNumber" : 2, + "internalID" : "0BC7FF5E", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "A higher level subscription.", + "displayName" : "Subscription Gold", + "locale" : "en_US" + } + ], + "productID" : "subscription_gold", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "subscription_gold", + "subscriptionGroupID" : "D0FEE8D8", + "type" : "RecurringSubscription" + } + ] + } + ], + "version" : { + "major" : 1, + "minor" : 0 + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Info.plist b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..a8f31ba92572 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + in_app_purchase_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/main.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/main.m new file mode 100644 index 000000000000..f97b9ef5c8a1 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/main.m @@ -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. + +#import +#import +#import "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/in_app_purchase_pluginTests/Info.plist b/packages/in_app_purchase/in_app_purchase_ios/example/ios/in_app_purchase_pluginTests/Info.plist new file mode 100644 index 000000000000..6c40a6cd0c4a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/in_app_purchase_pluginTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/lib/consumable_store.dart b/packages/in_app_purchase/in_app_purchase_ios/example/lib/consumable_store.dart new file mode 100644 index 000000000000..4d10a50e1ee8 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/lib/consumable_store.dart @@ -0,0 +1,51 @@ +// 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:shared_preferences/shared_preferences.dart'; + +/// A store of consumable items. +/// +/// This is a development prototype tha stores consumables in the shared +/// preferences. Do not use this in real world apps. +class ConsumableStore { + static const String _kPrefKey = 'consumables'; + static Future _writes = Future.value(); + + /// Adds a consumable with ID `id` to the store. + /// + /// The consumable is only added after the returned Future is complete. + static Future save(String id) { + _writes = _writes.then((void _) => _doSave(id)); + return _writes; + } + + /// Consumes a consumable with ID `id` from the store. + /// + /// The consumable was only consumed after the returned Future is complete. + static Future consume(String id) { + _writes = _writes.then((void _) => _doConsume(id)); + return _writes; + } + + /// Returns the list of consumables from the store. + static Future> load() async { + return (await SharedPreferences.getInstance()).getStringList(_kPrefKey) ?? + []; + } + + static Future _doSave(String id) async { + List cached = await load(); + SharedPreferences prefs = await SharedPreferences.getInstance(); + cached.add(id); + await prefs.setStringList(_kPrefKey, cached); + } + + static Future _doConsume(String id) async { + List cached = await load(); + SharedPreferences prefs = await SharedPreferences.getInstance(); + cached.remove(id); + await prefs.setStringList(_kPrefKey, cached); + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/lib/main.dart b/packages/in_app_purchase/in_app_purchase_ios/example/lib/main.dart new file mode 100644 index 000000000000..d91108c854c9 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/lib/main.dart @@ -0,0 +1,409 @@ +// 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 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; +import 'consumable_store.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + // When using the Android plugin directly it is mandatory to register + // the plugin as default instance as part of initializing the app. + InAppPurchaseIosPlatform.registerPlatform(); + + runApp(_MyApp()); +} + +const bool _kAutoConsume = true; + +const String _kConsumableId = 'consumable'; +const String _kUpgradeId = 'upgrade'; +const String _kSilverSubscriptionId = 'subscription_silver'; +const String _kGoldSubscriptionId = 'subscription_gold'; +const List _kProductIds = [ + _kConsumableId, + _kUpgradeId, + _kSilverSubscriptionId, + _kGoldSubscriptionId, +]; + +class _MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State<_MyApp> { + final InAppPurchaseIosPlatform _iapIosPlatform = + InAppPurchasePlatform.instance as InAppPurchaseIosPlatform; + late StreamSubscription> _subscription; + List _notFoundIds = []; + List _products = []; + List _purchases = []; + List _consumables = []; + bool _isAvailable = false; + bool _purchasePending = false; + bool _loading = true; + String? _queryProductError; + + @override + void initState() { + final Stream> purchaseUpdated = + _iapIosPlatform.purchaseStream; + _subscription = purchaseUpdated.listen((purchaseDetailsList) { + _listenToPurchaseUpdated(purchaseDetailsList); + }, onDone: () { + _subscription.cancel(); + }, onError: (error) { + // handle error here. + }); + initStoreInfo(); + super.initState(); + } + + Future initStoreInfo() async { + final bool isAvailable = await _iapIosPlatform.isAvailable(); + if (!isAvailable) { + setState(() { + _isAvailable = isAvailable; + _products = []; + _purchases = []; + _notFoundIds = []; + _consumables = []; + _purchasePending = false; + _loading = false; + }); + return; + } + + ProductDetailsResponse productDetailResponse = + await _iapIosPlatform.queryProductDetails(_kProductIds.toSet()); + if (productDetailResponse.error != null) { + setState(() { + _queryProductError = productDetailResponse.error!.message; + _isAvailable = isAvailable; + _products = productDetailResponse.productDetails; + _purchases = []; + _notFoundIds = productDetailResponse.notFoundIDs; + _consumables = []; + _purchasePending = false; + _loading = false; + }); + return; + } + + if (productDetailResponse.productDetails.isEmpty) { + setState(() { + _queryProductError = null; + _isAvailable = isAvailable; + _products = productDetailResponse.productDetails; + _purchases = []; + _notFoundIds = productDetailResponse.notFoundIDs; + _consumables = []; + _purchasePending = false; + _loading = false; + }); + return; + } + + await _iapIosPlatform.restorePurchases(); + + List consumables = await ConsumableStore.load(); + setState(() { + _isAvailable = isAvailable; + _products = productDetailResponse.productDetails; + _notFoundIds = productDetailResponse.notFoundIDs; + _consumables = consumables; + _purchasePending = false; + _loading = false; + }); + } + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + List stack = []; + if (_queryProductError == null) { + stack.add( + ListView( + children: [ + _buildConnectionCheckTile(), + _buildProductList(), + _buildConsumableBox(), + ], + ), + ); + } else { + stack.add(Center( + child: Text(_queryProductError!), + )); + } + if (_purchasePending) { + stack.add( + Stack( + children: [ + Opacity( + opacity: 0.3, + child: const ModalBarrier(dismissible: false, color: Colors.grey), + ), + Center( + child: CircularProgressIndicator(), + ), + ], + ), + ); + } + + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('IAP Example'), + ), + body: Stack( + children: stack, + ), + ), + ); + } + + Card _buildConnectionCheckTile() { + if (_loading) { + return Card(child: ListTile(title: const Text('Trying to connect...'))); + } + final Widget storeHeader = ListTile( + leading: Icon(_isAvailable ? Icons.check : Icons.block, + color: _isAvailable ? Colors.green : ThemeData.light().errorColor), + title: Text( + 'The store is ' + (_isAvailable ? 'available' : 'unavailable') + '.'), + ); + final List children = [storeHeader]; + + if (!_isAvailable) { + children.addAll([ + Divider(), + ListTile( + title: Text('Not connected', + style: TextStyle(color: ThemeData.light().errorColor)), + subtitle: const Text( + 'Unable to connect to the payments processor. Has this app been configured correctly? See the example README for instructions.'), + ), + ]); + } + return Card(child: Column(children: children)); + } + + Card _buildProductList() { + if (_loading) { + return Card( + child: (ListTile( + leading: CircularProgressIndicator(), + title: Text('Fetching products...')))); + } + if (!_isAvailable) { + return Card(); + } + final ListTile productHeader = ListTile(title: Text('Products for Sale')); + List productList = []; + if (_notFoundIds.isNotEmpty) { + productList.add(ListTile( + title: Text('[${_notFoundIds.join(", ")}] not found', + style: TextStyle(color: ThemeData.light().errorColor)), + subtitle: Text( + 'This app needs special configuration to run. Please see example/README.md for instructions.'))); + } + + // This loading previous purchases code is just a demo. Please do not use this as it is. + // In your app you should always verify the purchase data using the `verificationData` inside the [PurchaseDetails] object before trusting it. + // We recommend that you use your own server to verify the purchase data. + Map purchases = + Map.fromEntries(_purchases.map((PurchaseDetails purchase) { + if (purchase.pendingCompletePurchase) { + _iapIosPlatform.completePurchase(purchase); + } + return MapEntry(purchase.productID, purchase); + })); + productList.addAll(_products.map( + (ProductDetails productDetails) { + PurchaseDetails? previousPurchase = purchases[productDetails.id]; + return ListTile( + title: Text( + productDetails.title, + ), + subtitle: Text( + productDetails.description, + ), + trailing: previousPurchase != null + ? Icon(Icons.check) + : TextButton( + child: Text(productDetails.price), + style: TextButton.styleFrom( + backgroundColor: Colors.green[800], + primary: Colors.white, + ), + onPressed: () { + // NOTE: If you are making a subscription purchase/upgrade/downgrade, we recommend you to + // verify the latest status of you your subscription by using server side receipt validation + // and update the UI accordingly. The subscription purchase status shown + // inside the app may not be accurate. + final oldSubscription = + _getOldSubscription(productDetails, purchases); + PurchaseParam purchaseParam = PurchaseParam( + productDetails: productDetails, + applicationUserName: null, + ); + if (productDetails.id == _kConsumableId) { + _iapIosPlatform.buyConsumable( + purchaseParam: purchaseParam, + autoConsume: _kAutoConsume || Platform.isIOS); + } else { + _iapIosPlatform.buyNonConsumable( + purchaseParam: purchaseParam); + } + }, + )); + }, + )); + + return Card( + child: + Column(children: [productHeader, Divider()] + productList)); + } + + Card _buildConsumableBox() { + if (_loading) { + return Card( + child: (ListTile( + leading: CircularProgressIndicator(), + title: Text('Fetching consumables...')))); + } + if (!_isAvailable || _notFoundIds.contains(_kConsumableId)) { + return Card(); + } + final ListTile consumableHeader = + ListTile(title: Text('Purchased consumables')); + final List tokens = _consumables.map((String id) { + return GridTile( + child: IconButton( + icon: Icon( + Icons.stars, + size: 42.0, + color: Colors.orange, + ), + splashColor: Colors.yellowAccent, + onPressed: () => consume(id), + ), + ); + }).toList(); + return Card( + child: Column(children: [ + consumableHeader, + Divider(), + GridView.count( + crossAxisCount: 5, + children: tokens, + shrinkWrap: true, + padding: EdgeInsets.all(16.0), + ) + ])); + } + + Future consume(String id) async { + await ConsumableStore.consume(id); + final List consumables = await ConsumableStore.load(); + setState(() { + _consumables = consumables; + }); + } + + void showPendingUI() { + setState(() { + _purchasePending = true; + }); + } + + void deliverProduct(PurchaseDetails purchaseDetails) async { + // IMPORTANT!! Always verify purchase details before delivering the product. + if (purchaseDetails.productID == _kConsumableId) { + await ConsumableStore.save(purchaseDetails.purchaseID!); + List consumables = await ConsumableStore.load(); + setState(() { + _purchasePending = false; + _consumables = consumables; + }); + } else { + setState(() { + _purchases.add(purchaseDetails); + _purchasePending = false; + }); + } + } + + void handleError(IAPError error) { + setState(() { + _purchasePending = false; + }); + } + + Future _verifyPurchase(PurchaseDetails purchaseDetails) { + // IMPORTANT!! Always verify a purchase before delivering the product. + // For the purpose of an example, we directly return true. + return Future.value(true); + } + + void _handleInvalidPurchase(PurchaseDetails purchaseDetails) { + // handle invalid purchase here if _verifyPurchase` failed. + } + + void _listenToPurchaseUpdated(List purchaseDetailsList) { + purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async { + if (purchaseDetails.status == PurchaseStatus.pending) { + showPendingUI(); + } else { + if (purchaseDetails.status == PurchaseStatus.error) { + handleError(purchaseDetails.error!); + } else if (purchaseDetails.status == PurchaseStatus.purchased) { + bool valid = await _verifyPurchase(purchaseDetails); + if (valid) { + deliverProduct(purchaseDetails); + } else { + _handleInvalidPurchase(purchaseDetails); + return; + } + } + + if (purchaseDetails.pendingCompletePurchase) { + await _iapIosPlatform.completePurchase(purchaseDetails); + } + } + }); + } + + PurchaseDetails? _getOldSubscription( + ProductDetails productDetails, Map purchases) { + // This is just to demonstrate a subscription upgrade or downgrade. + // This method assumes that you have only 2 subscriptions under a group, 'subscription_silver' & 'subscription_gold'. + // The 'subscription_silver' subscription can be upgraded to 'subscription_gold' and + // the 'subscription_gold' subscription can be downgraded to 'subscription_silver'. + // Please remember to replace the logic of finding the old subscription Id as per your app. + // The old subscription is only required on Android since Apple handles this internally + // by using the subscription group feature in iTunesConnect. + PurchaseDetails? oldSubscription; + if (productDetails.id == _kSilverSubscriptionId && + purchases[_kGoldSubscriptionId] != null) { + oldSubscription = purchases[_kGoldSubscriptionId]; + } else if (productDetails.id == _kGoldSubscriptionId && + purchases[_kSilverSubscriptionId] != null) { + oldSubscription = purchases[_kSilverSubscriptionId]; + } + return oldSubscription; + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/example/pubspec.yaml new file mode 100644 index 000000000000..339e2c1ff0e2 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/pubspec.yaml @@ -0,0 +1,33 @@ +name: in_app_purchase_ios_example +description: Demonstrates how to use the in_app_purchase_ios plugin. +publish_to: none + +dependencies: + flutter: + sdk: flutter + shared_preferences: ^2.0.0-nullsafety.1 + in_app_purchase_ios: + # When depending on this package from a real application you should use: + # in_app_purchase: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # TODO(mvanbeusekom): Replace with pub.dev version when in_app_purchase_platform_interface is published + in_app_purchase_platform_interface: + path: ../../in_app_purchase_platform_interface + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + +flutter: + uses-material-design: true + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.9.1+hotfix.2" diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/test_driver/test/integration_test.dart b/packages/in_app_purchase/in_app_purchase_ios/example/test_driver/test/integration_test.dart new file mode 100644 index 000000000000..4c4c006068b8 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/test_driver/test/integration_test.dart @@ -0,0 +1,18 @@ +// 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. + +// @dart = 2.9 +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter_driver/flutter_driver.dart'; + +Future main() async { + final FlutterDriver driver = await FlutterDriver.connect(); + final String data = + await driver.requestData(null, timeout: const Duration(minutes: 1)); + await driver.close(); + final Map result = jsonDecode(data); + exit(result['result'] == 'true' ? 0 : 1); +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/InAppPurchasePluginTest.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/InAppPurchasePluginTest.m index cb00cbc2a43e..176d8e41f880 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/InAppPurchasePluginTest.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/InAppPurchasePluginTest.m @@ -7,7 +7,7 @@ #import "FIAPaymentQueueHandler.h" #import "Stubs.h" -@import in_app_purchase; +@import in_app_purchase_ios; @interface InAppPurchasePluginTest : XCTestCase diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/PaymentQueueTest.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/PaymentQueueTest.m index c335fa3ef307..6cfbd278a429 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/PaymentQueueTest.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/PaymentQueueTest.m @@ -5,7 +5,7 @@ #import #import "Stubs.h" -@import in_app_purchase; +@import in_app_purchase_ios; @interface PaymentQueueTest : XCTestCase diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/ProductRequestHandlerTest.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/ProductRequestHandlerTest.m index 19f5848b7168..16b9462ce11d 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/ProductRequestHandlerTest.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/ProductRequestHandlerTest.m @@ -5,7 +5,7 @@ #import #import "Stubs.h" -@import in_app_purchase; +@import in_app_purchase_ios; #pragma tests start here diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/Stubs.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/Stubs.h index e07cc3f5a147..60c481980dff 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/Stubs.h +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/Stubs.h @@ -5,7 +5,7 @@ #import #import -@import in_app_purchase; +@import in_app_purchase_ios; NS_ASSUME_NONNULL_BEGIN API_AVAILABLE(ios(11.2), macos(10.13.2)) diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/TranslatorTest.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/TranslatorTest.m index 550d1fc341c6..385d29140e49 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/TranslatorTest.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Tests/TranslatorTest.m @@ -5,7 +5,7 @@ #import #import "Stubs.h" -@import in_app_purchase; +@import in_app_purchase_ios; @interface TranslatorTest : XCTestCase diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase.podspec b/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase_ios.podspec similarity index 96% rename from packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase.podspec rename to packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase_ios.podspec index 4a423dd036cf..1ccc87fd2f04 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase.podspec +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase_ios.podspec @@ -2,7 +2,7 @@ # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html # Pod::Spec.new do |s| - s.name = 'in_app_purchase' + s.name = 'in_app_purchase_ios' s.version = '0.0.1' s.summary = 'Flutter In App Purchase' s.description = <<-DESC diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml index a5351530f019..6fec6d906a74 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: flutter: sdk: flutter + json_annotation: ^4.0.0 meta: ^1.3.0 test: ^1.16.0 From 3eb649b3871f161308dd75f1a894e5b518d7d5c9 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 10 May 2021 16:25:24 +0200 Subject: [PATCH 32/33] Added iOS integration test --- .../in_app_purchase_test.dart | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/integration_test/in_app_purchase_test.dart diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/integration_test/in_app_purchase_test.dart b/packages/in_app_purchase/in_app_purchase_ios/example/integration_test/in_app_purchase_test.dart new file mode 100644 index 000000000000..e626f4f074a6 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/integration_test/in_app_purchase_test.dart @@ -0,0 +1,21 @@ +// 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. + +// @dart = 2.9 +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Can create InAppPurchaseAndroid instance', + (WidgetTester tester) async { + InAppPurchaseIosPlatform.registerPlatform(); + final InAppPurchasePlatform androidPlatform = + InAppPurchasePlatform.instance; + expect(androidPlatform, isNotNull); + }); +} From ab4eaa090eab36fb12c9a9846d43444daf093771 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 12 May 2021 07:36:25 +0200 Subject: [PATCH 33/33] Update example dependencies --- .../in_app_purchase_ios/example/pubspec.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/example/pubspec.yaml index 339e2c1ff0e2..b0287fc021fc 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/example/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: none dependencies: flutter: sdk: flutter - shared_preferences: ^2.0.0-nullsafety.1 + shared_preferences: ^2.0.0 in_app_purchase_ios: # When depending on this package from a real application you should use: # in_app_purchase: ^x.y.z @@ -14,9 +14,7 @@ dependencies: # the parent directory to use the current plugin's version. path: ../ - # TODO(mvanbeusekom): Replace with pub.dev version when in_app_purchase_platform_interface is published - in_app_purchase_platform_interface: - path: ../../in_app_purchase_platform_interface + in_app_purchase_platform_interface: ^1.0.0 dev_dependencies: flutter_driver: