blob: 4c4953d1ce98e29e3346819ede175ebc6360a735 [file] [log] [blame]
// Copyright 2019 The Chromium 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 'app_store_connection.dart';
import 'google_play_connection.dart';
import 'product_details.dart';
import 'package:flutter/foundation.dart';
import 'package:in_app_purchase/billing_client_wrappers.dart';
import './purchase_details.dart';
export 'package:in_app_purchase/billing_client_wrappers.dart';
/// Basic API for making in app purchases across multiple platforms.
///
/// This is a generic abstraction built from `billing_client_wrapers` and
/// `store_kit_wrappers`. Either library can be used for their respective
/// platform instead of this.
abstract class InAppPurchaseConnection {
/// 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 => _getStream();
Stream<List<PurchaseDetails>> _purchaseUpdatedStream;
Stream<List<PurchaseDetails>> _getStream() {
if (_purchaseUpdatedStream != null) {
return _purchaseUpdatedStream;
}
if (Platform.isAndroid) {
_purchaseUpdatedStream =
GooglePlayConnection.instance.purchaseUpdatedStream;
} else if (Platform.isIOS) {
_purchaseUpdatedStream =
AppStoreConnection.instance.purchaseUpdatedStream;
} else {
throw UnsupportedError(
'InAppPurchase plugin only works on Android and iOS.');
}
return _purchaseUpdatedStream;
}
/// Whether pending purchase is enabled.
///
/// See also [enablePendingPurchases] for more on pending purchases.
static bool get enablePendingPurchase => _enablePendingPurchase;
static bool _enablePendingPurchase = false;
/// Returns true if the payment platform is ready and available.
Future<bool> isAvailable();
/// Enable the [InAppPurchaseConnection] to handle pending purchases.
///
/// Android Only: This method is required to be called when initialize the application.
/// It is to acknowledge your application has been updated to support pending purchases.
/// See [Support pending transactions](https://developer.android.com/google/play/billing/billing_library_overview#pending)
/// for more details.
/// Failure to call this method before access [instance] will throw an exception.
///
/// It is an no-op on iOS.
static void enablePendingPurchases() {
_enablePendingPurchase = true;
}
/// 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);
/// 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. On iOS, you also need to 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});
/// 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<bool> buyConsumable(
{@required PurchaseParam purchaseParam, bool autoConsume = true});
/// 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].
/// Completing a [PurchaseStatus.pending] purchase will cause an exception.
/// For convenience, [PurchaseDetails.pendingCompletePurchase] indicates if a purchase is pending for completion.
///
/// The method returns a [BillingResultWrapper] to indicate a detailed status of the complete process.
/// If the result contains [BillingResponse.error] or [BillingResponse.serviceUnavailable], the developer should try
/// to complete the purchase via this method again, or retry the [completePurchase] it at a later time.
/// If the result indicates other errors, there might be some issue with
/// the app's code. The developer is responsible to fix the issue.
///
/// 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.
///
/// The optional parameter `developerPayload` only works on Android.
Future<BillingResultWrapper> completePurchase(PurchaseDetails purchase,
{String developerPayload});
/// (Play only) Mark that the user has consumed a product.
///
/// You are responsible for consuming all consumable purchases once they are
/// delivered. The user won't be able to buy the same product again until the
/// purchase of the product is consumed.
///
/// The `developerPayload` can be specified to be associated with this consumption.
///
/// This throws an [UnsupportedError] on iOS.
Future<BillingResultWrapper> consumePurchase(PurchaseDetails purchase,
{String developerPayload});
/// Query all previous purchases.
///
/// The `applicationUserName` should match whatever was sent in the initial
/// `PurchaseParam`, if anything.
///
/// 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});
/// (App Store only) retry loading purchase data after an initial failure.
///
/// Throws an [UnsupportedError] on Android.
Future<PurchaseVerificationData> refreshPurchaseVerificationData();
/// The [InAppPurchaseConnection] implemented for this platform.
///
/// Throws an [UnsupportedError] when accessed on a platform other than
/// Android or iOS.
static InAppPurchaseConnection get instance => _getOrCreateInstance();
static InAppPurchaseConnection _instance;
static InAppPurchaseConnection _getOrCreateInstance() {
if (_instance != null) {
return _instance;
}
if (Platform.isAndroid) {
_instance = GooglePlayConnection.instance;
} else if (Platform.isIOS) {
_instance = AppStoreConnection.instance;
} else {
throw UnsupportedError(
'InAppPurchase plugin only works on Android and iOS.');
}
return _instance;
}
}
/// Which platform the request is on.
enum IAPSource { GooglePlay, AppStore }
/// 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 {
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, possibly null.
final String message;
/// Error details, possibly null.
final dynamic details;
}