[in_app_purchase] Only register transactionObservers when someone is listening to purchaseUpdates (#4035)
* Start and stop payment queue when (no longer) listened to
* Added Flutter unit tests
* Added native iOS unit tests
* Added changelog and pubspec
* Update packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m
Co-authored-by: Maurits van Beusekom <maurits@vnbskm.nl>
* Update packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart
Co-authored-by: Maurits van Beusekom <maurits@vnbskm.nl>
* Improve documentation on stopObservingTransactionQueue
* formatting
Co-authored-by: Maurits van Beusekom <maurits@vnbskm.nl>
diff --git a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md
index 480426c..4b2d8ce 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md
+++ b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.1.0+2
+
+* Changed the iOS payment queue handler in such a way that it only adds a listener to the SKPaymentQueue when there
+ is a listener to the Dart purchaseStream.
+
## 0.1.0+1
* Added a "Restore purchases" button to conform to Apple's StoreKit guidelines on [restoring products](https://developer.apple.com/documentation/storekit/in-app_purchase/restoring_purchased_products?language=objc);
diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m
index 6e436e4..241ea0d 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m
+++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m
@@ -301,4 +301,46 @@
XCTAssertEqualObjects(resultArray, @[ transactionMap ]);
}
+- (void)testStartAndStopObservingPaymentQueue {
+ FlutterMethodCall* startCall = [FlutterMethodCall
+ methodCallWithMethodName:@"-[SKPaymentQueue startObservingTransactionQueue]"
+ arguments:nil];
+ FlutterMethodCall* stopCall =
+ [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue stopObservingTransactionQueue]"
+ arguments:nil];
+
+ SKPaymentQueueStub* queue = [SKPaymentQueueStub new];
+
+ self.plugin.paymentQueueHandler =
+ [[FIAPaymentQueueHandler alloc] initWithQueue:queue
+ transactionsUpdated:nil
+ transactionRemoved:nil
+ restoreTransactionFailed:nil
+ restoreCompletedTransactionsFinished:nil
+ shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment,
+ SKProduct* _Nonnull product) {
+ return YES;
+ }
+ updatedDownloads:nil];
+
+ // Check that there is no observer to start with.
+ XCTAssertNil(queue.observer);
+
+ // Start observing
+ [self.plugin handleMethodCall:startCall
+ result:^(id r){
+ }];
+
+ // Observer should be set
+ XCTAssertNotNil(queue.observer);
+
+ // Stop observing
+ [self.plugin handleMethodCall:stopCall
+ result:^(id r){
+ }];
+
+ // No observer should be set
+ XCTAssertNil(queue.observer);
+}
+
@end
diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h
index 60c4819..687118f 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h
+++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h
@@ -36,6 +36,7 @@
@interface SKPaymentQueueStub : SKPaymentQueue
@property(assign, nonatomic) SKPaymentTransactionState testState;
+@property(strong, nonatomic, nullable) id<SKPaymentTransactionObserver> observer;
@end
@interface SKPaymentTransactionStub : SKPaymentTransaction
diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m
index 66610a8..8af326a 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m
+++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m
@@ -151,8 +151,6 @@
@interface SKPaymentQueueStub ()
-@property(strong, nonatomic) id<SKPaymentTransactionObserver> observer;
-
@end
@implementation SKPaymentQueueStub
@@ -161,6 +159,10 @@
self.observer = observer;
}
+- (void)removeTransactionObserver:(id<SKPaymentTransactionObserver>)observer {
+ self.observer = nil;
+}
+
- (void)addPayment:(SKPayment *)payment {
SKPaymentTransactionStub *transaction =
[[SKPaymentTransactionStub alloc] initWithState:self.testState payment:payment];
diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h
index fddeb07..30865b2 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h
+++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h
@@ -34,6 +34,8 @@
// This method needs to be called before any other methods.
- (void)startObservingPaymentQueue;
+// Call this method when the Flutter app is no longer listening
+- (void)stopObservingPaymentQueue;
// Appends a payment to the SKPaymentQueue.
//
diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m
index eb3348e..20ccbc5 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m
+++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m
@@ -44,6 +44,10 @@
[_queue addTransactionObserver:self];
}
+- (void)stopObservingPaymentQueue {
+ [_queue removeTransactionObserver:self];
+}
+
- (BOOL)addPayment:(SKPayment *)payment {
for (SKPaymentTransaction *transaction in self.queue.transactions) {
if ([transaction.payment.productIdentifier isEqualToString:payment.productIdentifier]) {
diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m
index 9034fe6..8a998d9 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m
+++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m
@@ -73,7 +73,6 @@
updatedDownloads:^void(NSArray<SKDownload *> *_Nonnull downloads) {
[weakSelf updatedDownloads:downloads];
}];
- [_paymentQueueHandler startObservingPaymentQueue];
_callbackChannel =
[FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase"
binaryMessenger:[registrar messenger]];
@@ -100,6 +99,10 @@
[self retrieveReceiptData:call result:result];
} else if ([@"-[InAppPurchasePlugin refreshReceipt:result:]" isEqualToString:call.method]) {
[self refreshReceipt:call result:result];
+ } else if ([@"-[SKPaymentQueue startObservingTransactionQueue]" isEqualToString:call.method]) {
+ [_paymentQueueHandler startObservingPaymentQueue];
+ } else if ([@"-[SKPaymentQueue stopObservingTransactionQueue]" isEqualToString:call.method]) {
+ [_paymentQueueHandler stopObservingPaymentQueue];
} else {
result(FlutterMethodNotImplemented);
}
diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart
index a83c887..74bb898 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart
+++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart
@@ -51,7 +51,15 @@
InAppPurchasePlatform.instance = InAppPurchaseIosPlatform();
_skPaymentQueueWrapper = SKPaymentQueueWrapper();
- _observer = _TransactionObserver(StreamController.broadcast());
+
+ // Create a purchaseUpdatedController and notify the native side when to
+ // start of stop sending updates.
+ StreamController<List<PurchaseDetails>> updateController =
+ StreamController.broadcast(
+ onListen: () => _skPaymentQueueWrapper.startObservingTransactionQueue(),
+ onCancel: () => _skPaymentQueueWrapper.stopObservingTransactionQueue(),
+ );
+ _observer = _TransactionObserver(updateController);
_skPaymentQueueWrapper.setTransactionObserver(observer);
}
diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart
index b677772..fe5f14b 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart
+++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart
@@ -11,6 +11,7 @@
import 'package:meta/meta.dart';
import '../channel.dart';
+import '../in_app_purchase_ios_platform.dart';
import 'sk_payment_transaction_wrappers.dart';
import 'sk_product_wrapper.dart';
@@ -64,6 +65,24 @@
channel.setMethodCallHandler(_handleObserverCallbacks);
}
+ /// Instructs the iOS implementation to register a transaction observer and
+ /// start listening to it.
+ ///
+ /// Call this method when the first listener is subscribed to the
+ /// [InAppPurchaseIosPlatform.purchaseStream].
+ Future startObservingTransactionQueue() async =>
+ await channel.invokeListMethod<void>(
+ '-[SKPaymentQueue startObservingTransactionQueue]');
+
+ /// Instructs the iOS implementation to remove the transaction observer and
+ /// stop listening to it.
+ ///
+ /// Call this when there are no longer any listeners subscribed to the
+ /// [InAppPurchaseIosPlatform.purchaseStream].
+ Future stopObservingTransactionQueue() async =>
+ await channel.invokeListMethod<void>(
+ '-[SKPaymentQueue stopObservingTransactionQueue]');
+
/// Posts a payment to the queue.
///
/// This sends a purchase request to the App Store for confirmation.
diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml
index 7a3885a..5b9e389 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml
+++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml
@@ -2,7 +2,7 @@
description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the iOS StoreKit Framework.
repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22
-version: 0.1.0+1
+version: 0.1.0+2
environment:
sdk: ">=2.12.0 <3.0.0"
diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart b/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart
index f392413..ac5c499 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart
+++ b/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart
@@ -29,6 +29,7 @@
PlatformException? queryProductException;
PlatformException? restoreException;
SKError? testRestoredError;
+ bool queueIsActive = false;
void reset() {
transactions = [];
@@ -66,6 +67,7 @@
queryProductException = null;
restoreException = null;
testRestoredError = null;
+ queueIsActive = false;
}
SKPaymentTransactionWrapper createPendingTransaction(String id) {
@@ -176,6 +178,12 @@
call.arguments["productIdentifier"],
call.arguments["transactionIdentifier"]));
break;
+ case '-[SKPaymentQueue startObservingTransactionQueue]':
+ queueIsActive = true;
+ break;
+ case '-[SKPaymentQueue stopObservingTransactionQueue]':
+ queueIsActive = false;
+ break;
}
return Future<void>.sync(() {});
}
diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart
index b15249c..973b9d1 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart
+++ b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart
@@ -302,4 +302,19 @@
expect(fakeIOSPlatform.finishedTransactions.length, 1);
});
});
+
+ group('purchase stream', () {
+ test('Should only have active queue when purchaseStream has listeners', () {
+ Stream<List<PurchaseDetails>> stream = iapIosPlatform.purchaseStream;
+ expect(fakeIOSPlatform.queueIsActive, false);
+ StreamSubscription subscription1 = stream.listen((event) {});
+ expect(fakeIOSPlatform.queueIsActive, true);
+ StreamSubscription subscription2 = stream.listen((event) {});
+ expect(fakeIOSPlatform.queueIsActive, true);
+ subscription1.cancel();
+ expect(fakeIOSPlatform.queueIsActive, true);
+ subscription2.cancel();
+ expect(fakeIOSPlatform.queueIsActive, false);
+ });
+ });
}
diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart
index e71279e..edb50ae 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart
+++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart
@@ -22,6 +22,7 @@
tearDown(() {
fakeIOSPlatform.testReturnNull = false;
+ fakeIOSPlatform.queueIsActive = null;
});
group('sk_request_maker', () {
@@ -132,6 +133,18 @@
await queue.restoreTransactions(applicationUserName: 'aUserID');
expect(fakeIOSPlatform.applicationNameHasTransactionRestored, 'aUserID');
});
+
+ test('startObservingTransactionQueue should call methodChannel', () async {
+ expect(fakeIOSPlatform.queueIsActive, isNot(true));
+ await SKPaymentQueueWrapper().startObservingTransactionQueue();
+ expect(fakeIOSPlatform.queueIsActive, true);
+ });
+
+ test('stopObservingTransactionQueue should call methodChannel', () async {
+ expect(fakeIOSPlatform.queueIsActive, isNot(false));
+ await SKPaymentQueueWrapper().stopObservingTransactionQueue();
+ expect(fakeIOSPlatform.queueIsActive, false);
+ });
});
group('Code Redemption Sheet', () {
@@ -165,6 +178,9 @@
// present Code Redemption
bool presentCodeRedemption = false;
+ // Listen to purchase updates
+ bool? queueIsActive;
+
Future<dynamic> onMethodCall(MethodCall call) {
switch (call.method) {
// request makers
@@ -208,8 +224,14 @@
case '-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]':
presentCodeRedemption = true;
return Future<void>.sync(() {});
+ case '-[SKPaymentQueue startObservingTransactionQueue]':
+ queueIsActive = true;
+ return Future<void>.sync(() {});
+ case '-[SKPaymentQueue stopObservingTransactionQueue]':
+ queueIsActive = false;
+ return Future<void>.sync(() {});
}
- return Future<void>.sync(() {});
+ return Future.error('method not mocked');
}
}