blob: 205e1b0653b4df924b24114e4e4e26e226e5ec3b [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:async';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart' as widgets;
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_purchase_android/billing_client_wrappers.dart';
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
import 'package:in_app_purchase_android/src/channel.dart';
import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart';
import 'billing_client_wrappers/product_details_wrapper_test.dart';
import 'billing_client_wrappers/purchase_wrapper_test.dart';
import 'stub_in_app_purchase_platform.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform();
late InAppPurchaseAndroidPlatform iapAndroidPlatform;
const String startConnectionCall =
'BillingClient#startConnection(BillingClientStateListener)';
const String endConnectionCall = 'BillingClient#endConnection()';
const String acknowledgePurchaseCall =
'BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)';
const String onBillingServiceDisconnectedCallback =
'BillingClientStateListener#onBillingServiceDisconnected()';
setUpAll(() {
_ambiguate(TestDefaultBinaryMessengerBinding.instance)!
.defaultBinaryMessenger
.setMockMethodCallHandler(channel, stubPlatform.fakeMethodCallHandler);
});
setUp(() {
widgets.WidgetsFlutterBinding.ensureInitialized();
const String debugMessage = 'dummy message';
const BillingResponse responseCode = BillingResponse.ok;
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: responseCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: startConnectionCall,
value: buildBillingResultMap(expectedBillingResult));
stubPlatform.addResponse(name: endConnectionCall);
InAppPurchaseAndroidPlatform.registerPlatform();
iapAndroidPlatform =
InAppPurchasePlatform.instance as InAppPurchaseAndroidPlatform;
});
tearDown(() {
stubPlatform.reset();
});
group('connection management', () {
test('connects on initialization', () {
//await iapAndroidPlatform.isAvailable();
expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(1));
});
test('re-connects when client sends onBillingServiceDisconnected', () {
iapAndroidPlatform.billingClientManager.client.callHandler(
const MethodCall(onBillingServiceDisconnectedCallback,
<String, dynamic>{'handle': 0}),
);
expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(2));
});
test(
're-connects when operation returns BillingResponse.clientDisconnected',
() async {
final Map<String, dynamic> okValue = buildBillingResultMap(
const BillingResultWrapper(responseCode: BillingResponse.ok));
stubPlatform.addResponse(
name: acknowledgePurchaseCall,
value: buildBillingResultMap(
const BillingResultWrapper(
responseCode: BillingResponse.serviceDisconnected,
),
),
);
stubPlatform.addResponse(
name: startConnectionCall,
value: okValue,
additionalStepBeforeReturn: (dynamic _) => stubPlatform.addResponse(
name: acknowledgePurchaseCall, value: okValue),
);
final PurchaseDetails purchase =
GooglePlayPurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase)
.first;
final BillingResultWrapper result =
await iapAndroidPlatform.completePurchase(purchase);
expect(
stubPlatform.countPreviousCalls(acknowledgePurchaseCall),
equals(2),
);
expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(2));
expect(result.responseCode, equals(BillingResponse.ok));
});
});
group('isAvailable', () {
test('true', () async {
stubPlatform.addResponse(name: 'BillingClient#isReady()', value: true);
expect(await iapAndroidPlatform.isAvailable(), isTrue);
});
test('false', () async {
stubPlatform.addResponse(name: 'BillingClient#isReady()', value: false);
expect(await iapAndroidPlatform.isAvailable(), isFalse);
});
});
group('queryProductDetails', () {
const String queryMethodName =
'BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener)';
test('handles empty productDetails', () async {
const String debugMessage = 'dummy message';
const BillingResponse responseCode = BillingResponse.ok;
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: responseCode, debugMessage: debugMessage);
stubPlatform.addResponse(name: queryMethodName, value: <String, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'productDetailsList': <Map<String, dynamic>>[],
});
final ProductDetailsResponse response =
await iapAndroidPlatform.queryProductDetails(<String>{''});
expect(response.productDetails, isEmpty);
});
test('should get correct product details', () async {
const String debugMessage = 'dummy message';
const BillingResponse responseCode = BillingResponse.ok;
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: responseCode, debugMessage: debugMessage);
stubPlatform.addResponse(name: queryMethodName, value: <String, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'productDetailsList': <Map<String, dynamic>>[
buildProductMap(dummyOneTimeProductDetails)
]
});
// Since queryProductDetails makes 2 platform method calls (one for each ProductType), the result will contain 2 dummyWrapper instead
// of 1.
final ProductDetailsResponse response =
await iapAndroidPlatform.queryProductDetails(<String>{'valid'});
expect(response.productDetails.first.title,
dummyOneTimeProductDetails.title);
expect(response.productDetails.first.description,
dummyOneTimeProductDetails.description);
expect(
response.productDetails.first.price,
dummyOneTimeProductDetails
.oneTimePurchaseOfferDetails?.formattedPrice);
expect(response.productDetails.first.currencySymbol, r'$');
});
test('should get the correct notFoundIDs', () async {
const String debugMessage = 'dummy message';
const BillingResponse responseCode = BillingResponse.ok;
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: responseCode, debugMessage: debugMessage);
stubPlatform.addResponse(name: queryMethodName, value: <String, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'productDetailsList': <Map<String, dynamic>>[
buildProductMap(dummyOneTimeProductDetails)
]
});
// Since queryProductDetails makes 2 platform method calls (one for each ProductType), the result will contain 2 dummyWrapper instead
// of 1.
final ProductDetailsResponse response =
await iapAndroidPlatform.queryProductDetails(<String>{'invalid'});
expect(response.notFoundIDs.first, 'invalid');
});
test(
'should have error stored in the response when platform exception is thrown',
() async {
const BillingResponse responseCode = BillingResponse.ok;
stubPlatform.addResponse(
name: queryMethodName,
value: <String, dynamic>{
'responseCode':
const BillingResponseConverter().toJson(responseCode),
'productDetailsList': <Map<String, dynamic>>[
buildProductMap(dummyOneTimeProductDetails)
]
},
additionalStepBeforeReturn: (dynamic _) {
throw PlatformException(
code: 'error_code',
message: 'error_message',
details: <dynamic, dynamic>{'info': 'error_info'},
);
});
// Since queryProductDetails makes 2 platform method calls (one for each ProductType), the result will contain 2 dummyWrapper instead
// of 1.
final ProductDetailsResponse response =
await iapAndroidPlatform.queryProductDetails(<String>{'invalid'});
expect(response.notFoundIDs, <String>['invalid']);
expect(response.productDetails, isEmpty);
expect(response.error, isNotNull);
expect(response.error!.source, kIAPSource);
expect(response.error!.code, 'error_code');
expect(response.error!.message, 'error_message');
expect(response.error!.details, <String, dynamic>{'info': 'error_info'});
});
});
group('restorePurchases', () {
const String queryMethodName =
'BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)';
test('handles error', () async {
const String debugMessage = 'dummy message';
const BillingResponse responseCode = BillingResponse.developerError;
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: responseCode, debugMessage: debugMessage);
stubPlatform.addResponse(name: queryMethodName, value: <dynamic, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(responseCode),
'purchasesList': <Map<String, dynamic>>[]
});
expect(
iapAndroidPlatform.restorePurchases(),
throwsA(
isA<InAppPurchaseException>()
.having(
(InAppPurchaseException e) => e.source, 'source', kIAPSource)
.having((InAppPurchaseException e) => e.code, 'code',
kRestoredPurchaseErrorCode)
.having((InAppPurchaseException e) => e.message, 'message',
responseCode.toString()),
),
);
});
test('should store platform exception in the response', () async {
const String debugMessage = 'dummy message';
const BillingResponse responseCode = BillingResponse.developerError;
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: responseCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: queryMethodName,
value: <dynamic, dynamic>{
'responseCode':
const BillingResponseConverter().toJson(responseCode),
'billingResult': buildBillingResultMap(expectedBillingResult),
'purchasesList': <Map<String, dynamic>>[]
},
additionalStepBeforeReturn: (dynamic _) {
throw PlatformException(
code: 'error_code',
message: 'error_message',
details: <dynamic, dynamic>{'info': 'error_info'},
);
});
expect(
iapAndroidPlatform.restorePurchases(),
throwsA(
isA<PlatformException>()
.having((PlatformException e) => e.code, 'code', 'error_code')
.having((PlatformException e) => e.message, 'message',
'error_message')
.having((PlatformException e) => e.details, 'details',
<String, dynamic>{'info': 'error_info'}),
),
);
});
test('returns ProductDetailsResponseWrapper', () async {
final Completer<List<PurchaseDetails>> completer =
Completer<List<PurchaseDetails>>();
final Stream<List<PurchaseDetails>> stream =
iapAndroidPlatform.purchaseStream;
late StreamSubscription<List<PurchaseDetails>> subscription;
subscription = stream.listen((List<PurchaseDetails> purchaseDetailsList) {
if (purchaseDetailsList.first.status == PurchaseStatus.restored) {
completer.complete(purchaseDetailsList);
subscription.cancel();
}
});
const String debugMessage = 'dummy message';
const BillingResponse responseCode = BillingResponse.ok;
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: responseCode, debugMessage: debugMessage);
stubPlatform.addResponse(name: queryMethodName, value: <String, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(responseCode),
'purchasesList': <Map<String, dynamic>>[
buildPurchaseMap(dummyPurchase),
]
});
// Since queryPastPurchases makes 2 platform method calls (one for each
// ProductType), the result will contain 2 dummyPurchase instances instead
// of 1.
await iapAndroidPlatform.restorePurchases();
final List<PurchaseDetails> restoredPurchases = await completer.future;
expect(restoredPurchases.length, 2);
for (final PurchaseDetails element in restoredPurchases) {
final GooglePlayPurchaseDetails purchase =
element as GooglePlayPurchaseDetails;
expect(purchase.productID, dummyPurchase.products.first);
expect(purchase.purchaseID, dummyPurchase.orderId);
expect(purchase.verificationData.localVerificationData,
dummyPurchase.originalJson);
expect(purchase.verificationData.serverVerificationData,
dummyPurchase.purchaseToken);
expect(purchase.verificationData.source, kIAPSource);
expect(purchase.transactionDate, dummyPurchase.purchaseTime.toString());
expect(purchase.billingClientPurchase, dummyPurchase);
expect(purchase.status, PurchaseStatus.restored);
}
});
});
group('make payment', () {
const String launchMethodName =
'BillingClient#launchBillingFlow(Activity, BillingFlowParams)';
const String consumeMethodName =
'BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener)';
test('buy non consumable, serializes and deserializes data', () async {
const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails;
const String accountId = 'hashedAccountId';
const String debugMessage = 'dummy message';
const BillingResponse sentCode = BillingResponse.ok;
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: sentCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: launchMethodName,
value: buildBillingResultMap(expectedBillingResult),
additionalStepBeforeReturn: (dynamic _) {
// Mock java update purchase callback.
final MethodCall call =
MethodCall(kOnPurchasesUpdated, <dynamic, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(sentCode),
'purchasesList': <dynamic>[
<dynamic, dynamic>{
'orderId': 'orderID1',
'products': <String>[productDetails.productId],
'isAutoRenewing': false,
'packageName': 'package',
'purchaseTime': 1231231231,
'purchaseToken': 'token',
'signature': 'sign',
'originalJson': 'json',
'developerPayload': 'dummy payload',
'isAcknowledged': true,
'purchaseState': 1,
}
]
});
iapAndroidPlatform.billingClientManager.client.callHandler(call);
});
final Completer<PurchaseDetails> completer = Completer<PurchaseDetails>();
PurchaseDetails purchaseDetails;
final Stream<List<PurchaseDetails>> purchaseStream =
iapAndroidPlatform.purchaseStream;
late StreamSubscription<List<PurchaseDetails>> subscription;
subscription = purchaseStream.listen((_) {
purchaseDetails = _.first;
completer.complete(purchaseDetails);
subscription.cancel();
}, onDone: () {});
final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam(
productDetails:
GooglePlayProductDetails.fromProductDetails(productDetails).first,
applicationUserName: accountId);
final bool launchResult = await iapAndroidPlatform.buyNonConsumable(
purchaseParam: purchaseParam);
final PurchaseDetails result = await completer.future;
expect(launchResult, isTrue);
expect(result.purchaseID, 'orderID1');
expect(result.status, PurchaseStatus.purchased);
expect(result.productID, productDetails.productId);
});
test('handles an error with an empty purchases list', () async {
const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails;
const String accountId = 'hashedAccountId';
const String debugMessage = 'dummy message';
const BillingResponse sentCode = BillingResponse.error;
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: sentCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: launchMethodName,
value: buildBillingResultMap(expectedBillingResult),
additionalStepBeforeReturn: (dynamic _) {
// Mock java update purchase callback.
final MethodCall call =
MethodCall(kOnPurchasesUpdated, <dynamic, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(sentCode),
'purchasesList': const <dynamic>[]
});
iapAndroidPlatform.billingClientManager.client.callHandler(call);
});
final Completer<PurchaseDetails> completer = Completer<PurchaseDetails>();
PurchaseDetails purchaseDetails;
final Stream<List<PurchaseDetails>> purchaseStream =
iapAndroidPlatform.purchaseStream;
late StreamSubscription<List<PurchaseDetails>> subscription;
subscription = purchaseStream.listen((_) {
purchaseDetails = _.first;
completer.complete(purchaseDetails);
subscription.cancel();
}, onDone: () {});
final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam(
productDetails:
GooglePlayProductDetails.fromProductDetails(productDetails).first,
applicationUserName: accountId);
await iapAndroidPlatform.buyNonConsumable(purchaseParam: purchaseParam);
final PurchaseDetails result = await completer.future;
expect(result.error, isNotNull);
expect(result.error!.source, kIAPSource);
expect(result.status, PurchaseStatus.error);
expect(result.purchaseID, isEmpty);
});
test('buy consumable with auto consume, serializes and deserializes data',
() async {
const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails;
const String accountId = 'hashedAccountId';
const String debugMessage = 'dummy message';
const BillingResponse sentCode = BillingResponse.ok;
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: sentCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: launchMethodName,
value: buildBillingResultMap(expectedBillingResult),
additionalStepBeforeReturn: (dynamic _) {
// Mock java update purchase callback.
final MethodCall call =
MethodCall(kOnPurchasesUpdated, <dynamic, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(sentCode),
'purchasesList': <dynamic>[
<dynamic, dynamic>{
'orderId': 'orderID1',
'products': <String>[productDetails.productId],
'isAutoRenewing': false,
'packageName': 'package',
'purchaseTime': 1231231231,
'purchaseToken': 'token',
'signature': 'sign',
'originalJson': 'json',
'developerPayload': 'dummy payload',
'isAcknowledged': true,
'purchaseState': 1,
}
]
});
iapAndroidPlatform.billingClientManager.client.callHandler(call);
});
final Completer<String> consumeCompleter = Completer<String>();
// adding call back for consume purchase
const BillingResponse expectedCode = BillingResponse.ok;
const BillingResultWrapper expectedBillingResultForConsume =
BillingResultWrapper(
responseCode: expectedCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: consumeMethodName,
value: buildBillingResultMap(expectedBillingResultForConsume),
additionalStepBeforeReturn: (dynamic args) {
final String purchaseToken =
(args as Map<Object?, Object?>)['purchaseToken']! as String;
consumeCompleter.complete(purchaseToken);
});
final Completer<PurchaseDetails> completer = Completer<PurchaseDetails>();
PurchaseDetails purchaseDetails;
final Stream<List<PurchaseDetails>> purchaseStream =
iapAndroidPlatform.purchaseStream;
late StreamSubscription<List<PurchaseDetails>> subscription;
subscription = purchaseStream.listen((_) {
purchaseDetails = _.first;
completer.complete(purchaseDetails);
subscription.cancel();
}, onDone: () {});
final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam(
productDetails:
GooglePlayProductDetails.fromProductDetails(productDetails).first,
applicationUserName: accountId);
final bool launchResult =
await iapAndroidPlatform.buyConsumable(purchaseParam: purchaseParam);
// Verify that the result has succeeded
final GooglePlayPurchaseDetails result =
await completer.future as GooglePlayPurchaseDetails;
expect(launchResult, isTrue);
expect(result.billingClientPurchase, isNotNull);
expect(result.billingClientPurchase.purchaseToken,
await consumeCompleter.future);
expect(result.status, PurchaseStatus.purchased);
expect(result.error, isNull);
});
test('buyNonConsumable propagates failures to launch the billing flow',
() async {
const String debugMessage = 'dummy message';
const BillingResponse sentCode = BillingResponse.error;
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: sentCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: launchMethodName,
value: buildBillingResultMap(expectedBillingResult));
final bool result = await iapAndroidPlatform.buyNonConsumable(
purchaseParam: GooglePlayPurchaseParam(
productDetails: GooglePlayProductDetails.fromProductDetails(
dummyOneTimeProductDetails)
.first));
// Verify that the failure has been converted and returned
expect(result, isFalse);
});
test('buyConsumable propagates failures to launch the billing flow',
() async {
const String debugMessage = 'dummy message';
const BillingResponse sentCode = BillingResponse.developerError;
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: sentCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: launchMethodName,
value: buildBillingResultMap(expectedBillingResult),
);
final bool result = await iapAndroidPlatform.buyConsumable(
purchaseParam: GooglePlayPurchaseParam(
productDetails: GooglePlayProductDetails.fromProductDetails(
dummyOneTimeProductDetails)
.first));
// Verify that the failure has been converted and returned
expect(result, isFalse);
});
test('adds consumption failures to PurchaseDetails objects', () async {
const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails;
const String accountId = 'hashedAccountId';
const String debugMessage = 'dummy message';
const BillingResponse sentCode = BillingResponse.ok;
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: sentCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: launchMethodName,
value: buildBillingResultMap(expectedBillingResult),
additionalStepBeforeReturn: (dynamic _) {
// Mock java update purchase callback.
final MethodCall call =
MethodCall(kOnPurchasesUpdated, <dynamic, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(sentCode),
'purchasesList': <dynamic>[
<dynamic, dynamic>{
'orderId': 'orderID1',
'products': <String>[productDetails.productId],
'isAutoRenewing': false,
'packageName': 'package',
'purchaseTime': 1231231231,
'purchaseToken': 'token',
'signature': 'sign',
'originalJson': 'json',
'developerPayload': 'dummy payload',
'isAcknowledged': true,
'purchaseState': 1,
}
]
});
iapAndroidPlatform.billingClientManager.client.callHandler(call);
});
final Completer<String> consumeCompleter = Completer<String>();
// adding call back for consume purchase
const BillingResponse expectedCode = BillingResponse.error;
const BillingResultWrapper expectedBillingResultForConsume =
BillingResultWrapper(
responseCode: expectedCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: consumeMethodName,
value: buildBillingResultMap(expectedBillingResultForConsume),
additionalStepBeforeReturn: (dynamic args) {
final String purchaseToken =
(args as Map<Object?, Object?>)['purchaseToken']! as String;
consumeCompleter.complete(purchaseToken);
});
final Completer<PurchaseDetails> completer = Completer<PurchaseDetails>();
PurchaseDetails purchaseDetails;
final Stream<List<PurchaseDetails>> purchaseStream =
iapAndroidPlatform.purchaseStream;
late StreamSubscription<List<PurchaseDetails>> subscription;
subscription = purchaseStream.listen((_) {
purchaseDetails = _.first;
completer.complete(purchaseDetails);
subscription.cancel();
}, onDone: () {});
final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam(
productDetails:
GooglePlayProductDetails.fromProductDetails(productDetails).first,
applicationUserName: accountId);
await iapAndroidPlatform.buyConsumable(purchaseParam: purchaseParam);
// Verify that the result has an error for the failed consumption
final GooglePlayPurchaseDetails result =
await completer.future as GooglePlayPurchaseDetails;
expect(result.billingClientPurchase, isNotNull);
expect(result.billingClientPurchase.purchaseToken,
await consumeCompleter.future);
expect(result.status, PurchaseStatus.error);
expect(result.error, isNotNull);
expect(result.error!.code, kConsumptionFailedErrorCode);
});
test(
'buy consumable without auto consume, consume api should not receive calls',
() async {
const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails;
const String accountId = 'hashedAccountId';
const String debugMessage = 'dummy message';
const BillingResponse sentCode = BillingResponse.developerError;
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: sentCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: launchMethodName,
value: buildBillingResultMap(expectedBillingResult),
additionalStepBeforeReturn: (dynamic _) {
// Mock java update purchase callback.
final MethodCall call =
MethodCall(kOnPurchasesUpdated, <dynamic, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(sentCode),
'purchasesList': <dynamic>[
<dynamic, dynamic>{
'orderId': 'orderID1',
'products': <String>[productDetails.productId],
'isAutoRenewing': false,
'packageName': 'package',
'purchaseTime': 1231231231,
'purchaseToken': 'token',
'signature': 'sign',
'originalJson': 'json',
'developerPayload': 'dummy payload',
'isAcknowledged': true,
'purchaseState': 1,
}
]
});
iapAndroidPlatform.billingClientManager.client.callHandler(call);
});
final Completer<String?> consumeCompleter = Completer<String?>();
// adding call back for consume purchase
const BillingResponse expectedCode = BillingResponse.ok;
const BillingResultWrapper expectedBillingResultForConsume =
BillingResultWrapper(
responseCode: expectedCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: consumeMethodName,
value: buildBillingResultMap(expectedBillingResultForConsume),
additionalStepBeforeReturn: (dynamic args) {
final String purchaseToken =
(args as Map<Object?, Object?>)['purchaseToken']! as String;
consumeCompleter.complete(purchaseToken);
});
final Stream<List<PurchaseDetails>> purchaseStream =
iapAndroidPlatform.purchaseStream;
late StreamSubscription<List<PurchaseDetails>> subscription;
subscription = purchaseStream.listen((_) {
consumeCompleter.complete(null);
subscription.cancel();
}, onDone: () {});
final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam(
productDetails:
GooglePlayProductDetails.fromProductDetails(productDetails).first,
applicationUserName: accountId);
await iapAndroidPlatform.buyConsumable(
purchaseParam: purchaseParam, autoConsume: false);
expect(null, await consumeCompleter.future);
});
test(
'should get canceled purchase status when response code is BillingResponse.userCanceled',
() async {
const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails;
const String accountId = 'hashedAccountId';
const String debugMessage = 'dummy message';
const BillingResponse sentCode = BillingResponse.userCanceled;
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: sentCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: launchMethodName,
value: buildBillingResultMap(expectedBillingResult),
additionalStepBeforeReturn: (dynamic _) {
// Mock java update purchase callback.
final MethodCall call =
MethodCall(kOnPurchasesUpdated, <dynamic, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(sentCode),
'purchasesList': <dynamic>[
<dynamic, dynamic>{
'orderId': 'orderID1',
'products': <String>[productDetails.productId],
'isAutoRenewing': false,
'packageName': 'package',
'purchaseTime': 1231231231,
'purchaseToken': 'token',
'signature': 'sign',
'originalJson': 'json',
'developerPayload': 'dummy payload',
'isAcknowledged': true,
'purchaseState': 1,
}
]
});
iapAndroidPlatform.billingClientManager.client.callHandler(call);
});
final Completer<String> consumeCompleter = Completer<String>();
// adding call back for consume purchase
const BillingResponse expectedCode = BillingResponse.userCanceled;
const BillingResultWrapper expectedBillingResultForConsume =
BillingResultWrapper(
responseCode: expectedCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: consumeMethodName,
value: buildBillingResultMap(expectedBillingResultForConsume),
additionalStepBeforeReturn: (dynamic args) {
final String purchaseToken =
(args as Map<Object?, Object?>)['purchaseToken']! as String;
consumeCompleter.complete(purchaseToken);
});
final Completer<PurchaseDetails> completer = Completer<PurchaseDetails>();
PurchaseDetails purchaseDetails;
final Stream<List<PurchaseDetails>> purchaseStream =
iapAndroidPlatform.purchaseStream;
late StreamSubscription<List<PurchaseDetails>> subscription;
subscription = purchaseStream.listen((_) {
purchaseDetails = _.first;
completer.complete(purchaseDetails);
subscription.cancel();
}, onDone: () {});
final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam(
productDetails:
GooglePlayProductDetails.fromProductDetails(productDetails).first,
applicationUserName: accountId);
await iapAndroidPlatform.buyConsumable(purchaseParam: purchaseParam);
// Verify that the result has an error for the failed consumption
final GooglePlayPurchaseDetails result =
await completer.future as GooglePlayPurchaseDetails;
expect(result.status, PurchaseStatus.canceled);
});
test(
'should get purchased purchase status when upgrading subscription by deferred proration mode',
() async {
const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails;
const String accountId = 'hashedAccountId';
const String debugMessage = 'dummy message';
const BillingResponse sentCode = BillingResponse.ok;
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: sentCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: launchMethodName,
value: buildBillingResultMap(expectedBillingResult),
additionalStepBeforeReturn: (dynamic _) {
// Mock java update purchase callback.
final MethodCall call =
MethodCall(kOnPurchasesUpdated, <dynamic, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': const BillingResponseConverter().toJson(sentCode),
'purchasesList': const <dynamic>[]
});
iapAndroidPlatform.billingClientManager.client.callHandler(call);
});
final Completer<PurchaseDetails> completer = Completer<PurchaseDetails>();
PurchaseDetails purchaseDetails;
final Stream<List<PurchaseDetails>> purchaseStream =
iapAndroidPlatform.purchaseStream;
late StreamSubscription<List<PurchaseDetails>> subscription;
subscription = purchaseStream.listen((_) {
purchaseDetails = _.first;
completer.complete(purchaseDetails);
subscription.cancel();
}, onDone: () {});
final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam(
productDetails:
GooglePlayProductDetails.fromProductDetails(productDetails).first,
applicationUserName: accountId,
changeSubscriptionParam: ChangeSubscriptionParam(
oldPurchaseDetails: GooglePlayPurchaseDetails.fromPurchase(
dummyUnacknowledgedPurchase)
.first,
prorationMode: ProrationMode.deferred,
));
await iapAndroidPlatform.buyNonConsumable(purchaseParam: purchaseParam);
final PurchaseDetails result = await completer.future;
expect(result.status, PurchaseStatus.purchased);
});
});
group('complete purchase', () {
const String completeMethodName =
'BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)';
test('complete purchase success', () async {
const BillingResponse expectedCode = BillingResponse.ok;
const String debugMessage = 'dummy message';
const BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: expectedCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: completeMethodName,
value: buildBillingResultMap(expectedBillingResult),
);
final PurchaseDetails purchaseDetails =
GooglePlayPurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase)
.first;
final Completer<BillingResultWrapper> completer =
Completer<BillingResultWrapper>();
purchaseDetails.status = PurchaseStatus.purchased;
if (purchaseDetails.pendingCompletePurchase) {
final BillingResultWrapper billingResultWrapper =
await iapAndroidPlatform.completePurchase(purchaseDetails);
expect(billingResultWrapper, equals(expectedBillingResult));
completer.complete(billingResultWrapper);
}
expect(await completer.future, equals(expectedBillingResult));
});
});
}
/// 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;