Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Draft implementation of platform interface
  • Loading branch information
mvanbeusekom committed Apr 22, 2021
commit 652e5d1b1d350013b4541296780c62fbc33a68fe
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 1.0.0

- Initial open-source release.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Copyright 2013 The Flutter Authors. All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# in_app_purchase_platform_interface

A common platform interface for the [`in_app_purchase`][1] plugin.

This interface allows platform-specific implementations of the `in_app_purchase`
plugin, as well as the plugin itself, to ensure they are supporting the
same interface.

# Usage

To implement a new platform-specific implementation of `in_app_purchase`, extend
[`InAppPurchasePlatform`][2] with an implementation that performs the
platform-specific behavior, and when you register your plugin, set the default
`InAppPurchasePlatform` by calling
`InAppPurchasePlatform.instance = MyPlatformInAppPurchase()`.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also add doc for how to add additional features in a platform

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

# Note on breaking changes

Strongly prefer non-breaking changes (such as adding a method to the interface)
over breaking changes for this package.

See https://flutter.dev/go/platform-interface-breaking-changes for a discussion
on why a less-clean interface is preferable to a breaking change.

[1]: ../in_app_purchase
[2]: lib/in_app_purchase_platform_interface.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

export 'src/in_app_purchase_platform.dart';
export 'src/types/types.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:plugin_platform_interface/plugin_platform_interface.dart';

import 'noop_in_app_purchase.dart';
import 'types/types.dart';

/// The interface that implementations of in_app_purchase must implement.
///
/// Platform implementations should extend this class rather than implement it as `in_app_purchase`
/// does not consider newly added methods to be breaking changes. Extending this class
/// (using `extends`) ensures that the subclass will get the default implementation, while
/// platform implementations that `implements` this interface will be broken by newly added
/// [InAppPurchasePlatform] methods.
abstract class InAppPurchasePlatform extends PlatformInterface {
/// Constructs a UrlLauncherPlatform.
InAppPurchasePlatform() : super(token: _token);

static final Object _token = Object();

static InAppPurchasePlatform _instance = NoopInAppPurchase();

/// The default instance of [InAppPurchasePlatform] to use.
static InAppPurchasePlatform get instance => _instance;

/// Platform-specific plugins should set this with their own platform-specific
/// class that extends [InAppPurchasePlatform] when they register themselves.
// TODO(amirh): Extract common platform interface logic.
// https://github.com/flutter/flutter/issues/43368
static set instance(InAppPurchasePlatform instance) {
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<List<PurchaseDetails>> get purchaseUpdatedStream =>
throw UnimplementedError(
'purchaseUpdatedStream has not been implemented.');

/// Returns true if the payment platform is ready and available.
Future<bool> isAvailable() =>
throw UnimplementedError('isAvailable() has not been implemented.');

/// Query product details for the given set of IDs.
///
/// The [identifiers] need to exactly match existing configured product
/// identifiers in the underlying payment platform, whether that's [App Store
/// 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<ProductDetailsResponse> queryProductDetails(Set<String> identifiers) =>
throw UnimplementedError(
'queryProductDetails() had not been implemented.');

/// Buy a non consumable product or subscription.
///
/// Non consumable items can only be bought once. For example, a purchase that
/// unlocks a special content in your app. Subscriptions are also non
/// consumable products.
///
/// You always need to restore all the non consumable products for user when
/// they switch their phones.
///
/// This method does not return the result of the purchase. Instead, after
/// triggering this method, purchase updates will be sent to
/// [purchaseUpdatedStream]. You should [Stream.listen] to
/// [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<bool> buyNonConsumable({required PurchaseParam purchaseParam}) =>
throw UnimplementedError('buyNonConsumable() has not been implemented.');

/// Buy a consumable product.
///
/// Consumable items can be "consumed" to mark that they've been used and then
/// bought additional times. For example, a health potion.
///
/// To restore consumable purchases across devices, you should keep track of
/// those purchase on your own server and restore the purchase for your users.
/// Consumed products are no longer considered to be "owned" by payment
/// platforms and will not be delivered by calling [queryPastPurchases].
///
/// 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's explain what does autoConsume do, instead of saying it is only for Android. We can also provide an example of what it does on Android and iOS.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated this to:

  /// `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. 

We can give a more detailed explanation on how to handle consumables manually on the platform specific implementation and provide the examples you mentioned. If you don't agree please let me know and I can make the necessary changes.

/// 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[consumePurchase] is not part of the platform interface, let's remove this line

///
/// Calling this method for non consumable items will cause unwanted
/// behaviors!
Future<bool> buyConsumable({
required PurchaseParam purchaseParam,
bool autoConsume = true,
}) =>
throw UnimplementedError('buyConsumable() has not been implemented.');

// TODO(mvanbeusekom): Add definition for the `completePurchase` method. The
// current definition uses the Android specific `BillingResultWrapper` class
// which is not really platform generic and needs a solution.

/// Query all previous purchases.
///
/// The `applicationUserName` should match whatever was sent in the initial
/// `PurchaseParam`, if anything. If no `applicationUserName` was specified in the initial
/// `PurchaseParam`, use `null`.
///
/// This does not return consumed products. If you want to restore unused
/// consumable products, you need to persist consumable product information
/// for your user on your own server.
///
/// See also:
///
/// * [refreshPurchaseVerificationData], for reloading failed
/// [PurchaseDetails.verificationData].
Future<QueryPurchaseDetailsResponse> queryPastPurchases(
{String? applicationUserName}) =>
throw UnimplementedError('queryPastPurchase() has not been implemented.');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'in_app_purchase_platform.dart';

class NoopInAppPurchase extends InAppPurchasePlatform {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'in_app_purchase_source.dart';

/// Captures an error from the underlying purchase platform.
///
/// The error can happen during the purchase, restoring a purchase, or querying product.
/// Errors from restoring a purchase are not indicative of any errors during the original purchase.
/// See also:
/// * [ProductDetailsResponse] for error when querying product details.
/// * [PurchaseDetails] for error happened in purchase.
class IAPError {
/// Creates a new IAP error object with the given error details.
IAPError(
{required this.source,
required this.code,
required this.message,
this.details});

/// Which source is the error on.
final IAPSource source;

/// The error code.
final String code;

/// A human-readable error message.
final String message;

/// Error details, possibly null.
final dynamic details;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/// Which platform the request is on.
enum IAPSource {
/// Google's Play Store.
GooglePlay,

/// Apple's App Store.
AppStore
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/// The class represents the information of a product.
///
/// This class unifies the BillingClient's [SkuDetailsWrapper] and StoreKit's [SKProductWrapper]. You can use the common attributes in
/// 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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'in_app_purchase_error.dart';
import 'product_details.dart';

/// The response returned by [InAppPurchaseConnection.queryProductDetails].
///
/// A list of [ProductDetails] can be obtained from the this response.
class ProductDetailsResponse {
/// Creates a new [ProductDetailsResponse] with the provided response details.
ProductDetailsResponse(
{required this.productDetails, required this.notFoundIDs, this.error});

/// Each [ProductDetails] uniquely matches one valid identifier in [identifiers] of [InAppPurchaseConnection.queryProductDetails].
final List<ProductDetails> productDetails;

/// The list of identifiers that are in the `identifiers` of [InAppPurchaseConnection.queryProductDetails] but failed to be fetched.
///
/// There's multiple platform specific reasons that product information could fail to be fetched,
/// ranging from products not being correctly configured in the storefront to the queried IDs not existing.
final List<String> notFoundIDs;

/// A caught platform exception thrown while querying the purchases.
///
/// The value is `null` if there is no error.
///
/// It's possible for this to be null but for there still to be notFoundIds in cases where the request itself was a success but the
/// requested IDs could not be found.
final IAPError? error;
}
Loading