Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[in_app_purchase] Implementation of platform interface #3781

Merged
merged 17 commits into from
Apr 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
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
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,33 @@
# 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.setInstance(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

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
`InAppPurchasePlatformAddition.instance = MyPlatformInAppPurchaseAddition()`.

# 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
[3]: lib/in_app_purchase_platform_addition.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// 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/in_app_purchase_platform_addition.dart';
export 'src/in_app_purchase_platform_addition_provider.dart';
export 'src/types/types.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// 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 '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 InAppPurchasePlatform.
InAppPurchasePlatform() : super(token: _token);

static final Object _token = Object();

/// The instance of [InAppPurchasePlatform] to use.
///
/// 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.
///
/// 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 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
/// 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 purchaseStream =>
throw UnimplementedError('purchaseStream 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.
///
/// 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<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
/// [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.
///
/// See also:
///
/// * [buyConsumable], for buying a consumable product.
/// * [restorePurchases], 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 [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.
///
/// `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.
///
/// 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 [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.
/// * [restorePurchases], for restoring non consumable products.
///
/// 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.');

/// Mark that purchased content has been delivered to the user.
///
/// You are responsible for completing every [PurchaseDetails] whose
/// [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.
///
/// 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
/// went wrong.
Future<void> completePurchase(PurchaseDetails purchase) =>
throw UnimplementedError('completePurchase() has not been implemented.');

/// 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.
///
/// See also:
///
/// * [refreshPurchaseVerificationData], for reloading failed
/// [PurchaseDetails.verificationData].
Future<void> restorePurchases({String? applicationUserName}) =>
throw UnimplementedError('restorePurchases() has not been implemented.');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// 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 InAppPurchasePlatformAddition {
/// 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<void> 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;
}
Original file line number Diff line number Diff line change
@@ -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_platform_addition.dart';

/// The [InAppPurchasePlatformAdditionProvider] is responsible for providing
/// a platform specific [InAppPurchasePlatformAddition].
///
/// [InAppPurchasePlatformAddition] implementation contain platform specific
/// features that are not available from the platform idiomatic
/// [InAppPurchasePlatform] API.
abstract class InAppPurchasePlatformAdditionProvider {
/// Provides a platform specific implementation of the [InAppPurchasePlatformAddition]
/// class.
T getPlatformAddition<T extends InAppPurchasePlatformAddition>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 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.

/// 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 String 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,34 @@
// 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.
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.
///
/// 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.
///
/// 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.
///
/// 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, 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;
}
Loading