blob: 6ae002c825172937e0aa76d6771dc174185c7243 [file] [log] [blame]
// 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:io';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';
import 'package:in_app_purchase_storekit/src/channel.dart';
import 'package:in_app_purchase_storekit/store_kit_wrappers.dart';
import '../store_kit_wrappers/sk_test_stub_objects.dart';
class FakeStoreKitPlatform {
FakeStoreKitPlatform() {
_ambiguate(TestDefaultBinaryMessengerBinding.instance)!
.defaultBinaryMessenger
.setMockMethodCallHandler(channel, onMethodCall);
}
// pre-configured store information
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;
late int testTransactionCancel;
PlatformException? queryProductException;
PlatformException? restoreException;
SKError? testRestoredError;
bool queueIsActive = false;
Map<String, dynamic> discountReceived = <String, dynamic>{};
void reset() {
transactions = <SKPaymentTransactionWrapper>[];
receiptData = 'dummy base64data';
validProductIDs = <String>{'123', '456'};
validProducts = <String, SKProductWrapper>{};
for (final String validID in validProductIDs) {
final Map<String, dynamic> productWrapperMap =
buildProductMap(dummyProductWrapper);
productWrapperMap['productIdentifier'] = validID;
if (validID == '456') {
productWrapperMap['priceLocale'] = buildLocaleMap(noSymbolLocale);
}
validProducts[validID] = SKProductWrapper.fromJson(productWrapperMap);
}
finishedTransactions = <SKPaymentTransactionWrapper>[];
testRestoredTransactionsNull = false;
testTransactionFail = false;
testTransactionCancel = -1;
queryProductException = null;
restoreException = null;
testRestoredError = null;
queueIsActive = false;
discountReceived = <String, dynamic>{};
}
SKPaymentTransactionWrapper createPendingTransaction(String id,
{int quantity = 1}) {
return SKPaymentTransactionWrapper(
transactionIdentifier: '',
payment: SKPaymentWrapper(productIdentifier: id, quantity: quantity),
transactionState: SKPaymentTransactionStateWrapper.purchasing,
transactionTimeStamp: 123123.121,
);
}
SKPaymentTransactionWrapper createPurchasedTransaction(
String productId, String transactionId,
{int quantity = 1}) {
return SKPaymentTransactionWrapper(
payment:
SKPaymentWrapper(productIdentifier: productId, quantity: quantity),
transactionState: SKPaymentTransactionStateWrapper.purchased,
transactionTimeStamp: 123123.121,
transactionIdentifier: transactionId);
}
SKPaymentTransactionWrapper createFailedTransaction(String productId,
{int quantity = 1}) {
return SKPaymentTransactionWrapper(
transactionIdentifier: '',
payment:
SKPaymentWrapper(productIdentifier: productId, quantity: quantity),
transactionState: SKPaymentTransactionStateWrapper.failed,
transactionTimeStamp: 123123.121,
error: const SKError(
code: 0,
domain: 'ios_domain',
userInfo: <String, Object>{'message': 'an error message'}));
}
SKPaymentTransactionWrapper createCanceledTransaction(
String productId, int errorCode,
{int quantity = 1}) {
return SKPaymentTransactionWrapper(
transactionIdentifier: '',
payment:
SKPaymentWrapper(productIdentifier: productId, quantity: quantity),
transactionState: SKPaymentTransactionStateWrapper.failed,
transactionTimeStamp: 123123.121,
error: SKError(
code: errorCode,
domain: 'ios_domain',
userInfo: const <String, Object>{'message': 'an error message'}));
}
SKPaymentTransactionWrapper createRestoredTransaction(
String productId, String transactionId,
{int quantity = 1}) {
return SKPaymentTransactionWrapper(
payment:
SKPaymentWrapper(productIdentifier: productId, quantity: quantity),
transactionState: SKPaymentTransactionStateWrapper.restored,
transactionTimeStamp: 123123.121,
transactionIdentifier: transactionId);
}
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!;
}
final List<String> productIDS =
List.castFrom<dynamic, String>(call.arguments as List<dynamic>);
final List<String> invalidFound = <String>[];
final List<SKProductWrapper> products = <SKProductWrapper>[];
for (final String productID in productIDS) {
if (!validProductIDs.contains(productID)) {
invalidFound.add(productID);
} else {
products.add(validProducts[productID]!);
}
}
final 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) {
InAppPurchaseStoreKitPlatform.observer
.restoreCompletedTransactionsFailed(error: testRestoredError!);
return Future<void>.sync(() {});
}
if (!testRestoredTransactionsNull) {
InAppPurchaseStoreKitPlatform.observer
.updatedTransactions(transactions: transactions);
}
InAppPurchaseStoreKitPlatform.observer
.paymentQueueRestoreCompletedTransactionsFinished();
return Future<void>.sync(() {});
case '-[InAppPurchasePlugin retrieveReceiptData:result:]':
if (receiptData != null) {
return Future<String>.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:]':
final Map<String, Object?> arguments = _getArgumentDictionary(call);
final String id = arguments['productIdentifier']! as String;
final int quantity = arguments['quantity']! as int;
// Keep the received paymentDiscount parameter when testing payment with discount.
if (arguments['applicationUsername']! == 'userWithDiscount') {
final Map<dynamic, dynamic>? discountArgument =
arguments['paymentDiscount'] as Map<dynamic, dynamic>?;
if (discountArgument != null) {
discountReceived = discountArgument.cast<String, dynamic>();
} else {
discountReceived = <String, dynamic>{};
}
}
final SKPaymentTransactionWrapper transaction =
createPendingTransaction(id, quantity: quantity);
transactions.add(transaction);
InAppPurchaseStoreKitPlatform.observer.updatedTransactions(
transactions: <SKPaymentTransactionWrapper>[transaction]);
sleep(const Duration(milliseconds: 30));
if (testTransactionFail) {
final SKPaymentTransactionWrapper transactionFailed =
createFailedTransaction(id, quantity: quantity);
InAppPurchaseStoreKitPlatform.observer.updatedTransactions(
transactions: <SKPaymentTransactionWrapper>[transactionFailed]);
} else if (testTransactionCancel > 0) {
final SKPaymentTransactionWrapper transactionCanceled =
createCanceledTransaction(id, testTransactionCancel,
quantity: quantity);
InAppPurchaseStoreKitPlatform.observer.updatedTransactions(
transactions: <SKPaymentTransactionWrapper>[transactionCanceled]);
} else {
final SKPaymentTransactionWrapper transactionFinished =
createPurchasedTransaction(
id, transaction.transactionIdentifier ?? '',
quantity: quantity);
InAppPurchaseStoreKitPlatform.observer.updatedTransactions(
transactions: <SKPaymentTransactionWrapper>[transactionFinished]);
}
break;
case '-[InAppPurchasePlugin finishTransaction:result:]':
final Map<String, Object?> arguments = _getArgumentDictionary(call);
finishedTransactions.add(createPurchasedTransaction(
arguments['productIdentifier']! as String,
arguments['transactionIdentifier']! as String,
quantity: transactions.first.payment.quantity));
break;
case '-[SKPaymentQueue startObservingTransactionQueue]':
queueIsActive = true;
break;
case '-[SKPaymentQueue stopObservingTransactionQueue]':
queueIsActive = false;
break;
}
return Future<void>.sync(() {});
}
/// Returns the arguments of [call] as typed string-keyed Map.
///
/// This does not do any type validation, so is only safe to call if the
/// arguments are known to be a map.
Map<String, Object?> _getArgumentDictionary(MethodCall call) {
return (call.arguments as Map<Object?, Object?>).cast<String, Object?>();
}
}
/// This allows a value of type T or T? to be treated as a value of type T?.
///
/// We use this so that APIs that have become non-nullable can still be used
/// with `!` and `?` on the stable branch.
T? _ambiguate<T>(T? value) => value;