[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');
   }
 }