blob: bfcab085e26a6b00265eae4338c6ae9c5a692626 [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 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart' show TestWidgetsFlutterBinding;
import 'package:in_app_purchase/src/in_app_purchase/purchase_details.dart';
import 'package:test/test.dart';
import 'package:in_app_purchase/src/channel.dart';
import 'package:in_app_purchase/src/in_app_purchase/app_store_connection.dart';
import 'package:in_app_purchase/src/in_app_purchase/in_app_purchase_connection.dart';
import 'package:in_app_purchase/src/in_app_purchase/product_details.dart';
import 'package:in_app_purchase/store_kit_wrappers.dart';
import '../billing_client_wrappers/purchase_wrapper_test.dart';
import '../store_kit_wrappers/sk_test_stub_objects.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final FakeIOSPlatform fakeIOSPlatform = FakeIOSPlatform();
setUpAll(() {
SystemChannels.platform
.setMockMethodCallHandler(fakeIOSPlatform.onMethodCall);
});
setUp(() => fakeIOSPlatform.reset());
tearDown(() => fakeIOSPlatform.reset());
group('isAvailable', () {
test('true', () async {
expect(await AppStoreConnection.instance.isAvailable(), isTrue);
});
});
group('query product list', () {
test('should get product list and correct invalid identifiers', () async {
final AppStoreConnection connection = AppStoreConnection();
final ProductDetailsResponse response = await connection
.queryProductDetails(<String>['123', '456', '789'].toSet());
List<ProductDetails> products = response.productDetails;
expect(products.first.id, '123');
expect(products[1].id, '456');
expect(response.notFoundIDs, ['789']);
expect(response.error, isNull);
});
test(
'if query products throws error, should get error object in the response',
() async {
fakeIOSPlatform.queryProductException = PlatformException(
code: 'error_code',
message: 'error_message',
details: {'info': 'error_info'});
final AppStoreConnection connection = AppStoreConnection();
final ProductDetailsResponse response = await connection
.queryProductDetails(<String>['123', '456', '789'].toSet());
expect(response.productDetails, []);
expect(response.notFoundIDs, ['123', '456', '789']);
expect(response.error, isNotNull);
expect(response.error!.source, IAPSource.AppStore);
expect(response.error!.code, 'error_code');
expect(response.error!.message, 'error_message');
expect(response.error!.details, {'info': 'error_info'});
});
});
group('query purchases list', () {
test('should get purchase list', () async {
QueryPurchaseDetailsResponse response =
await AppStoreConnection.instance.queryPastPurchases();
expect(response.pastPurchases.length, 2);
expect(response.pastPurchases.first.purchaseID,
fakeIOSPlatform.transactions.first.transactionIdentifier);
expect(response.pastPurchases.last.purchaseID,
fakeIOSPlatform.transactions.last.transactionIdentifier);
expect(response.pastPurchases.first.purchaseID,
fakeIOSPlatform.transactions.first.transactionIdentifier);
expect(response.pastPurchases.last.purchaseID,
fakeIOSPlatform.transactions.last.transactionIdentifier);
expect(response.pastPurchases, isNotEmpty);
expect(response.pastPurchases.first.verificationData, isNotNull);
expect(
response.pastPurchases.first.verificationData.localVerificationData,
'dummy base64data');
expect(
response.pastPurchases.first.verificationData.serverVerificationData,
'dummy base64data');
expect(response.error, isNull);
});
test('queryPastPurchases should not block transaction updates', () async {
fakeIOSPlatform.transactions
.add(fakeIOSPlatform.createPurchasedTransaction('foo', 'bar'));
Completer completer = Completer();
Stream<List<PurchaseDetails>> stream =
AppStoreConnection.instance.purchaseUpdatedStream;
late StreamSubscription subscription;
subscription = stream.listen((purchaseDetailsList) {
if (purchaseDetailsList.first.status == PurchaseStatus.purchased) {
completer.complete(purchaseDetailsList);
subscription.cancel();
}
});
QueryPurchaseDetailsResponse response =
await AppStoreConnection.instance.queryPastPurchases();
List<PurchaseDetails> result = await completer.future;
expect(result.length, 1);
expect(result.first.productID, 'foo');
expect(response.error, isNull);
});
test('should get empty result if there is no restored transactions',
() async {
fakeIOSPlatform.testRestoredTransactionsNull = true;
QueryPurchaseDetailsResponse response =
await AppStoreConnection.instance.queryPastPurchases();
expect(response.pastPurchases, isEmpty);
expect(response.error, isNull);
fakeIOSPlatform.testRestoredTransactionsNull = false;
});
test('test restore error', () async {
fakeIOSPlatform.testRestoredError = SKError(
code: 123,
domain: 'error_test',
userInfo: {'message': 'errorMessage'});
QueryPurchaseDetailsResponse response =
await AppStoreConnection.instance.queryPastPurchases();
expect(response.pastPurchases, isEmpty);
expect(response.error, isNotNull);
expect(response.error!.source, IAPSource.AppStore);
expect(response.error!.message, 'error_test');
expect(response.error!.details, {'message': 'errorMessage'});
});
test('receipt error should populate null to verificationData.data',
() async {
fakeIOSPlatform.receiptData = null;
QueryPurchaseDetailsResponse response =
await AppStoreConnection.instance.queryPastPurchases();
expect(
response.pastPurchases.first.verificationData.localVerificationData,
isEmpty);
expect(
response.pastPurchases.first.verificationData.serverVerificationData,
isEmpty);
});
});
group('refresh receipt data', () {
test('should refresh receipt data', () async {
PurchaseVerificationData? receiptData =
await AppStoreConnection.instance.refreshPurchaseVerificationData();
expect(receiptData, isNotNull);
expect(receiptData!.source, IAPSource.AppStore);
expect(receiptData.localVerificationData, 'refreshed receipt data');
expect(receiptData.serverVerificationData, 'refreshed receipt data');
});
});
group('make payment', () {
test(
'buying non consumable, should get purchase objects in the purchase update callback',
() async {
List<PurchaseDetails> details = [];
Completer completer = Completer();
Stream<List<PurchaseDetails>> stream =
AppStoreConnection.instance.purchaseUpdatedStream;
late StreamSubscription subscription;
subscription = stream.listen((purchaseDetailsList) {
details.addAll(purchaseDetailsList);
if (purchaseDetailsList.first.status == PurchaseStatus.purchased) {
completer.complete(details);
subscription.cancel();
}
});
final PurchaseParam purchaseParam = PurchaseParam(
productDetails: ProductDetails.fromSKProduct(dummyProductWrapper),
applicationUserName: 'appName');
await AppStoreConnection.instance
.buyNonConsumable(purchaseParam: purchaseParam);
List<PurchaseDetails> result = await completer.future;
expect(result.length, 2);
expect(result.first.productID, dummyProductWrapper.productIdentifier);
});
test(
'buying consumable, should get purchase objects in the purchase update callback',
() async {
List<PurchaseDetails> details = [];
Completer completer = Completer();
Stream<List<PurchaseDetails>> stream =
AppStoreConnection.instance.purchaseUpdatedStream;
late StreamSubscription subscription;
subscription = stream.listen((purchaseDetailsList) {
details.addAll(purchaseDetailsList);
if (purchaseDetailsList.first.status == PurchaseStatus.purchased) {
completer.complete(details);
subscription.cancel();
}
});
final PurchaseParam purchaseParam = PurchaseParam(
productDetails: ProductDetails.fromSKProduct(dummyProductWrapper),
applicationUserName: 'appName');
await AppStoreConnection.instance
.buyConsumable(purchaseParam: purchaseParam);
List<PurchaseDetails> result = await completer.future;
expect(result.length, 2);
expect(result.first.productID, dummyProductWrapper.productIdentifier);
});
test('buying consumable, should throw when autoConsume is false', () async {
final PurchaseParam purchaseParam = PurchaseParam(
productDetails: ProductDetails.fromSKProduct(dummyProductWrapper),
applicationUserName: 'appName');
expect(
() => AppStoreConnection.instance
.buyConsumable(purchaseParam: purchaseParam, autoConsume: false),
throwsA(TypeMatcher<AssertionError>()));
});
test('should get failed purchase status', () async {
fakeIOSPlatform.testTransactionFail = true;
List<PurchaseDetails> details = [];
Completer completer = Completer();
late IAPError error;
Stream<List<PurchaseDetails>> stream =
AppStoreConnection.instance.purchaseUpdatedStream;
late StreamSubscription subscription;
subscription = stream.listen((purchaseDetailsList) {
details.addAll(purchaseDetailsList);
purchaseDetailsList.forEach((purchaseDetails) {
if (purchaseDetails.status == PurchaseStatus.error) {
error = purchaseDetails.error!;
completer.complete(error);
subscription.cancel();
}
});
});
final PurchaseParam purchaseParam = PurchaseParam(
productDetails: ProductDetails.fromSKProduct(dummyProductWrapper),
applicationUserName: 'appName');
await AppStoreConnection.instance
.buyNonConsumable(purchaseParam: purchaseParam);
IAPError completerError = await completer.future;
expect(completerError.code, 'purchase_error');
expect(completerError.source, IAPSource.AppStore);
expect(completerError.message, 'ios_domain');
expect(completerError.details, {'message': 'an error message'});
});
});
group('complete purchase', () {
test('should complete purchase', () async {
List<PurchaseDetails> details = [];
Completer completer = Completer();
Stream<List<PurchaseDetails>> stream =
AppStoreConnection.instance.purchaseUpdatedStream;
late StreamSubscription subscription;
subscription = stream.listen((purchaseDetailsList) {
details.addAll(purchaseDetailsList);
purchaseDetailsList.forEach((purchaseDetails) {
if (purchaseDetails.pendingCompletePurchase) {
AppStoreConnection.instance.completePurchase(purchaseDetails);
completer.complete(details);
subscription.cancel();
}
});
});
final PurchaseParam purchaseParam = PurchaseParam(
productDetails: ProductDetails.fromSKProduct(dummyProductWrapper),
applicationUserName: 'appName');
await AppStoreConnection.instance
.buyNonConsumable(purchaseParam: purchaseParam);
List<PurchaseDetails> result = await completer.future;
expect(result.length, 2);
expect(result.first.productID, dummyProductWrapper.productIdentifier);
expect(fakeIOSPlatform.finishedTransactions.length, 1);
});
});
group('consume purchase', () {
test('should throw when calling consume purchase on iOS', () async {
expect(
() => AppStoreConnection.instance
.consumePurchase(PurchaseDetails.fromPurchase(dummyPurchase)),
throwsUnsupportedError);
});
});
}
class FakeIOSPlatform {
FakeIOSPlatform() {
channel.setMockMethodCallHandler(onMethodCall);
}
// pre-configured store informations
String? receiptData;
late Set<String> validProductIDs;
late Map<String, SKProductWrapper> validProducts;
late List<SKPaymentTransactionWrapper> transactions;
late List<SKPaymentTransactionWrapper> finishedTransactions;
late bool testRestoredTransactionsNull;
late bool testTransactionFail;
PlatformException? queryProductException;
PlatformException? restoreException;
SKError? testRestoredError;
void reset() {
transactions = [];
receiptData = 'dummy base64data';
validProductIDs = ['123', '456'].toSet();
validProducts = Map();
for (String validID in validProductIDs) {
Map<String, dynamic> productWrapperMap =
buildProductMap(dummyProductWrapper);
productWrapperMap['productIdentifier'] = validID;
validProducts[validID] = SKProductWrapper.fromJson(productWrapperMap);
}
SKPaymentTransactionWrapper tran1 = SKPaymentTransactionWrapper(
transactionIdentifier: '123',
payment: dummyPayment,
originalTransaction: dummyTransaction,
transactionTimeStamp: 123123123.022,
transactionState: SKPaymentTransactionStateWrapper.restored,
error: null,
);
SKPaymentTransactionWrapper tran2 = SKPaymentTransactionWrapper(
transactionIdentifier: '1234',
payment: dummyPayment,
originalTransaction: dummyTransaction,
transactionTimeStamp: 123123123.022,
transactionState: SKPaymentTransactionStateWrapper.restored,
error: null,
);
transactions.addAll([tran1, tran2]);
finishedTransactions = [];
testRestoredTransactionsNull = false;
testTransactionFail = false;
queryProductException = null;
restoreException = null;
testRestoredError = null;
}
SKPaymentTransactionWrapper createPendingTransaction(String id) {
return SKPaymentTransactionWrapper(
transactionIdentifier: '',
payment: SKPaymentWrapper(productIdentifier: id),
transactionState: SKPaymentTransactionStateWrapper.purchasing,
transactionTimeStamp: 123123.121,
error: null,
originalTransaction: null);
}
SKPaymentTransactionWrapper createPurchasedTransaction(
String productId, String transactionId) {
return SKPaymentTransactionWrapper(
payment: SKPaymentWrapper(productIdentifier: productId),
transactionState: SKPaymentTransactionStateWrapper.purchased,
transactionTimeStamp: 123123.121,
transactionIdentifier: transactionId,
error: null,
originalTransaction: null);
}
SKPaymentTransactionWrapper createFailedTransaction(String productId) {
return SKPaymentTransactionWrapper(
transactionIdentifier: '',
payment: SKPaymentWrapper(productIdentifier: productId),
transactionState: SKPaymentTransactionStateWrapper.failed,
transactionTimeStamp: 123123.121,
error: SKError(
code: 0,
domain: 'ios_domain',
userInfo: {'message': 'an error message'}),
originalTransaction: null);
}
Future<dynamic> onMethodCall(MethodCall call) {
switch (call.method) {
case '-[SKPaymentQueue canMakePayments:]':
return Future<bool>.value(true);
case '-[InAppPurchasePlugin startProductRequest:result:]':
if (queryProductException != null) {
throw queryProductException!;
}
List<String> productIDS =
List.castFrom<dynamic, String>(call.arguments);
assert(productIDS is List<String>, 'invalid argument type');
List<String> invalidFound = [];
List<SKProductWrapper> products = [];
for (String productID in productIDS) {
if (!validProductIDs.contains(productID)) {
invalidFound.add(productID);
} else {
products.add(validProducts[productID]!);
}
}
SkProductResponseWrapper response = SkProductResponseWrapper(
products: products, invalidProductIdentifiers: invalidFound);
return Future<Map<String, dynamic>>.value(
buildProductResponseMap(response));
case '-[InAppPurchasePlugin restoreTransactions:result:]':
if (restoreException != null) {
throw restoreException!;
}
if (testRestoredError != null) {
AppStoreConnection.observer
.restoreCompletedTransactionsFailed(error: testRestoredError!);
return Future<void>.sync(() {});
}
if (!testRestoredTransactionsNull) {
AppStoreConnection.observer
.updatedTransactions(transactions: transactions);
}
AppStoreConnection.observer
.paymentQueueRestoreCompletedTransactionsFinished();
return Future<void>.sync(() {});
case '-[InAppPurchasePlugin retrieveReceiptData:result:]':
if (receiptData != null) {
return Future<void>.value(receiptData);
} else {
throw PlatformException(code: 'no_receipt_data');
}
case '-[InAppPurchasePlugin refreshReceipt:result:]':
receiptData = 'refreshed receipt data';
return Future<void>.sync(() {});
case '-[InAppPurchasePlugin addPayment:result:]':
String id = call.arguments['productIdentifier'];
SKPaymentTransactionWrapper transaction = createPendingTransaction(id);
AppStoreConnection.observer
.updatedTransactions(transactions: [transaction]);
sleep(const Duration(milliseconds: 30));
if (testTransactionFail) {
SKPaymentTransactionWrapper transaction_failed =
createFailedTransaction(id);
AppStoreConnection.observer
.updatedTransactions(transactions: [transaction_failed]);
} else {
SKPaymentTransactionWrapper transaction_finished =
createPurchasedTransaction(
id, transaction.transactionIdentifier ?? '');
AppStoreConnection.observer
.updatedTransactions(transactions: [transaction_finished]);
}
break;
case '-[InAppPurchasePlugin finishTransaction:result:]':
finishedTransactions.add(createPurchasedTransaction(
call.arguments["productIdentifier"],
call.arguments["transactionIdentifier"]));
break;
}
return Future<void>.sync(() {});
}
}