blob: 871879dca08eb82ed2e1ff02f3cbcfec224c34d1 [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 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:in_app_purchase/src/in_app_purchase/purchase_details.dart';
import 'in_app_purchase_connection.dart';
import 'product_details.dart';
import 'package:in_app_purchase/store_kit_wrappers.dart';
import 'package:in_app_purchase/src/store_kit_wrappers/enum_converters.dart';
import '../../billing_client_wrappers.dart';
/// An [InAppPurchaseConnection] that wraps StoreKit.
///
/// This translates various `StoreKit` calls and responses into the
/// generic plugin API.
class AppStoreConnection implements InAppPurchaseConnection {
static AppStoreConnection get instance => _getOrCreateInstance();
static AppStoreConnection _instance;
static SKPaymentQueueWrapper _skPaymentQueueWrapper;
static _TransactionObserver _observer;
Stream<List<PurchaseDetails>> get purchaseUpdatedStream =>
_observer.purchaseUpdatedController.stream;
static SKTransactionObserverWrapper get observer => _observer;
static AppStoreConnection _getOrCreateInstance() {
if (_instance != null) {
return _instance;
}
_instance = AppStoreConnection();
_skPaymentQueueWrapper = SKPaymentQueueWrapper();
_observer = _TransactionObserver(StreamController.broadcast());
_skPaymentQueueWrapper.setTransactionObserver(observer);
return _instance;
}
@override
Future<bool> isAvailable() => SKPaymentQueueWrapper.canMakePayments();
@override
Future<bool> buyNonConsumable({@required PurchaseParam purchaseParam}) async {
await _skPaymentQueueWrapper.addPayment(SKPaymentWrapper(
productIdentifier: purchaseParam.productDetails.id,
quantity: 1,
applicationUsername: purchaseParam.applicationUserName,
simulatesAskToBuyInSandbox: purchaseParam.sandboxTesting,
requestData: null));
return true; // There's no error feedback from iOS here to return.
}
@override
Future<bool> buyConsumable(
{@required PurchaseParam purchaseParam, bool autoConsume = true}) {
assert(autoConsume == true, 'On iOS, we should always auto consume');
return buyNonConsumable(purchaseParam: purchaseParam);
}
@override
Future<BillingResultWrapper> completePurchase(PurchaseDetails purchase,
{String developerPayload}) async {
await _skPaymentQueueWrapper
.finishTransaction(purchase.skPaymentTransaction);
return BillingResultWrapper(responseCode: BillingResponse.ok);
}
@override
Future<BillingResultWrapper> consumePurchase(PurchaseDetails purchase,
{String developerPayload}) {
throw UnsupportedError('consume purchase is not available on Android');
}
@override
Future<QueryPurchaseDetailsResponse> queryPastPurchases(
{String applicationUserName}) async {
IAPError error;
List<PurchaseDetails> pastPurchases = [];
try {
String receiptData = await _observer.getReceiptData();
final List<SKPaymentTransactionWrapper> restoredTransactions =
await _observer.getRestoredTransactions(
queue: _skPaymentQueueWrapper,
applicationUserName: applicationUserName);
_observer.cleanUpRestoredTransactions();
pastPurchases =
restoredTransactions.map((SKPaymentTransactionWrapper transaction) {
assert(transaction.transactionState ==
SKPaymentTransactionStateWrapper.restored);
return PurchaseDetails.fromSKTransaction(transaction, receiptData)
..status = SKTransactionStatusConverter()
.toPurchaseStatus(transaction.transactionState)
..error = transaction.error != null
? IAPError(
source: IAPSource.AppStore,
code: kPurchaseErrorCode,
message: transaction.error.domain,
details: transaction.error.userInfo,
)
: null;
}).toList();
} on PlatformException catch (e) {
error = IAPError(
source: IAPSource.AppStore,
code: e.code,
message: e.message,
details: e.details);
} on SKError catch (e) {
error = IAPError(
source: IAPSource.AppStore,
code: kRestoredPurchaseErrorCode,
message: e.domain,
details: e.userInfo);
}
return QueryPurchaseDetailsResponse(
pastPurchases: pastPurchases, error: error);
}
@override
Future<PurchaseVerificationData> refreshPurchaseVerificationData() async {
await SKRequestMaker().startRefreshReceiptRequest();
String receipt = await SKReceiptManager.retrieveReceiptData();
return PurchaseVerificationData(
localVerificationData: receipt,
serverVerificationData: receipt,
source: IAPSource.AppStore);
}
/// 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<ProductDetailsResponse> queryProductDetails(
Set<String> 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> productDetails = [];
if (response.products != null) {
productDetails = response.products
.map((SKProductWrapper productWrapper) =>
ProductDetails.fromSKProduct(productWrapper))
.toList();
}
List<String> invalidIdentifiers = response.invalidProductIdentifiers ?? [];
if (productDetails.isEmpty) {
invalidIdentifiers = identifiers.toList();
}
ProductDetailsResponse productDetailsResponse = ProductDetailsResponse(
productDetails: productDetails,
notFoundIDs: invalidIdentifiers,
error: exception == null
? null
: IAPError(
source: IAPSource.AppStore,
code: exception.code,
message: exception.message,
details: exception.details),
);
return productDetailsResponse;
}
}
class _TransactionObserver implements SKTransactionObserverWrapper {
final StreamController<List<PurchaseDetails>> purchaseUpdatedController;
Completer<List<SKPaymentTransactionWrapper>> _restoreCompleter;
List<SKPaymentTransactionWrapper> _restoredTransactions;
String _receiptData;
_TransactionObserver(this.purchaseUpdatedController);
Future<List<SKPaymentTransactionWrapper>> getRestoredTransactions(
{@required SKPaymentQueueWrapper queue, String applicationUserName}) {
assert(queue != null);
_restoreCompleter = Completer();
queue.restoreTransactions(applicationUserName: applicationUserName);
return _restoreCompleter.future;
}
void cleanUpRestoredTransactions() {
_restoredTransactions = null;
_restoreCompleter = null;
}
void updatedTransactions(
{List<SKPaymentTransactionWrapper> transactions}) async {
if (_restoreCompleter != null) {
if (_restoredTransactions == null) {
_restoredTransactions = [];
}
_restoredTransactions
.addAll(transactions.where((SKPaymentTransactionWrapper wrapper) {
return wrapper.transactionState ==
SKPaymentTransactionStateWrapper.restored;
}).map((SKPaymentTransactionWrapper wrapper) => wrapper));
return;
}
String receiptData = await getReceiptData();
purchaseUpdatedController
.add(transactions.map((SKPaymentTransactionWrapper transaction) {
PurchaseDetails purchaseDetails =
PurchaseDetails.fromSKTransaction(transaction, receiptData);
return purchaseDetails;
}).toList());
}
void removedTransactions({List<SKPaymentTransactionWrapper> transactions}) {}
/// Triggered when there is an error while restoring transactions.
void restoreCompletedTransactionsFailed({SKError error}) {
_restoreCompleter.completeError(error);
}
void paymentQueueRestoreCompletedTransactionsFinished() {
_restoreCompleter.complete(_restoredTransactions ?? []);
}
bool shouldAddStorePayment(
{SKPaymentWrapper payment, SKProductWrapper product}) {
// In this unified API, we always return true to keep it consistent with the behavior on Google Play.
return true;
}
Future<String> getReceiptData() async {
try {
_receiptData = await SKReceiptManager.retrieveReceiptData();
} catch (e) {
_receiptData = null;
}
return _receiptData;
}
}