| // 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 { |
| /// Returns the singleton instance of the [AppStoreConnection] that should be |
| /// used across the app. |
| static AppStoreConnection get instance => _getOrCreateInstance(); |
| static AppStoreConnection? _instance; |
| static late SKPaymentQueueWrapper _skPaymentQueueWrapper; |
| static late _TransactionObserver _observer; |
| |
| /// Creates an [AppStoreConnection] object. |
| /// |
| /// This constructor should only be used for testing, for any other purpose |
| /// get the connection from the [instance] getter. |
| @visibleForTesting |
| AppStoreConnection(); |
| |
| Stream<List<PurchaseDetails>> get purchaseUpdatedStream => |
| _observer.purchaseUpdatedController.stream; |
| |
| /// Callback handler for transaction status changes. |
| @visibleForTesting |
| 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.simulatesAskToBuyInSandbox || |
| // ignore: deprecated_member_use_from_same_package |
| 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 { |
| if (purchase.skPaymentTransaction == null) { |
| throw ArgumentError( |
| 'completePurchase unsuccessful. The `purchase.skPaymentTransaction` is not valid'); |
| } |
| 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); |
| 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(); |
| _observer.cleanUpRestoredTransactions(); |
| } 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(); |
| final String? receipt = await SKReceiptManager.retrieveReceiptData(); |
| if (receipt == null) { |
| return null; |
| } |
| 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 = |
| <SKPaymentTransactionWrapper>[]; |
| late String _receiptData; |
| |
| _TransactionObserver(this.purchaseUpdatedController); |
| |
| Future<List<SKPaymentTransactionWrapper>> getRestoredTransactions( |
| {required SKPaymentQueueWrapper queue, String? applicationUserName}) { |
| _restoreCompleter = Completer(); |
| queue.restoreTransactions(applicationUserName: applicationUserName); |
| return _restoreCompleter!.future; |
| } |
| |
| void cleanUpRestoredTransactions() { |
| _restoredTransactions.clear(); |
| _restoreCompleter = null; |
| } |
| |
| void updatedTransactions( |
| {required 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)); |
| } |
| |
| String receiptData = await getReceiptData(); |
| purchaseUpdatedController |
| .add(transactions.where((SKPaymentTransactionWrapper wrapper) { |
| return wrapper.transactionState != |
| SKPaymentTransactionStateWrapper.restored; |
| }).map((SKPaymentTransactionWrapper transaction) { |
| PurchaseDetails purchaseDetails = |
| PurchaseDetails.fromSKTransaction(transaction, receiptData); |
| return purchaseDetails; |
| }).toList()); |
| } |
| |
| void removedTransactions( |
| {required List<SKPaymentTransactionWrapper> transactions}) {} |
| |
| /// Triggered when there is an error while restoring transactions. |
| void restoreCompletedTransactionsFailed({required SKError error}) { |
| _restoreCompleter!.completeError(error); |
| } |
| |
| void paymentQueueRestoreCompletedTransactionsFinished() { |
| _restoreCompleter!.complete(_restoredTransactions); |
| } |
| |
| 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<String> getReceiptData() async { |
| try { |
| _receiptData = await SKReceiptManager.retrieveReceiptData(); |
| } catch (e) { |
| _receiptData = ''; |
| } |
| return _receiptData; |
| } |
| } |