[in_app_purchase] Emit empty list when no transactions to restore. (#4523)

diff --git a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md
index ff5bffd..e565812 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md
+++ b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.3.0
+
+* **BREAKING CHANGE:** `InAppPurchaseStoreKitPlatform.restorePurchase()` emits an empty instance of `List<ProductDetails>` when there were no transactions to restore, indicating that the restore procedure has finished. 
+
 ## 0.2.1
 
 * Renames `in_app_purchase_ios` to `in_app_purchase_storekit` to facilitate
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart
index 65b1a30..e912dd6 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart
+++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart
@@ -153,11 +153,19 @@
   }
 }
 
+enum _TransactionRestoreState {
+  notRunning,
+  waitingForTransactions,
+  receivedTransaction,
+}
+
 class _TransactionObserver implements SKTransactionObserverWrapper {
   final StreamController<List<PurchaseDetails>> purchaseUpdatedController;
 
   Completer? _restoreCompleter;
   late String _receiptData;
+  _TransactionRestoreState _transactionRestoreState =
+      _TransactionRestoreState.notRunning;
 
   _TransactionObserver(this.purchaseUpdatedController);
 
@@ -165,6 +173,7 @@
     required SKPaymentQueueWrapper queue,
     String? applicationUserName,
   }) {
+    _transactionRestoreState = _TransactionRestoreState.waitingForTransactions;
     _restoreCompleter = Completer();
     queue.restoreTransactions(applicationUserName: applicationUserName);
     return _restoreCompleter!.future;
@@ -176,6 +185,14 @@
 
   void updatedTransactions(
       {required List<SKPaymentTransactionWrapper> transactions}) async {
+    if (_transactionRestoreState ==
+            _TransactionRestoreState.waitingForTransactions &&
+        transactions.any((transaction) =>
+            transaction.transactionState ==
+            SKPaymentTransactionStateWrapper.restored)) {
+      _transactionRestoreState = _TransactionRestoreState.receivedTransaction;
+    }
+
     String receiptData = await getReceiptData();
     List<PurchaseDetails> purchases = transactions
         .map((SKPaymentTransactionWrapper transaction) =>
@@ -191,10 +208,21 @@
   /// Triggered when there is an error while restoring transactions.
   void restoreCompletedTransactionsFailed({required SKError error}) {
     _restoreCompleter!.completeError(error);
+    _transactionRestoreState = _TransactionRestoreState.notRunning;
   }
 
   void paymentQueueRestoreCompletedTransactionsFinished() {
     _restoreCompleter!.complete();
+
+    // If no restored transactions were received during the restore session
+    // emit an empty list of purchase details to inform listeners that the
+    // restore session finished without any results.
+    if (_transactionRestoreState ==
+        _TransactionRestoreState.waitingForTransactions) {
+      purchaseUpdatedController.add(<PurchaseDetails>[]);
+    }
+
+    _transactionRestoreState = _TransactionRestoreState.notRunning;
   }
 
   bool shouldAddStorePayment(
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml
index bfc55b9..1fe291d 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml
+++ b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml
@@ -2,7 +2,7 @@
 description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the StoreKit Framework.
 repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_storekit
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22
-version: 0.2.1
+version: 0.3.0
 
 environment:
   sdk: ">=2.14.0 <3.0.0"
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart
index c4254c2..352ba32 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart
+++ b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart
@@ -47,24 +47,6 @@
       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;
@@ -123,6 +105,17 @@
         originalTransaction: null);
   }
 
+  SKPaymentTransactionWrapper createRestoredTransaction(
+      String productId, String transactionId) {
+    return SKPaymentTransactionWrapper(
+        payment: SKPaymentWrapper(productIdentifier: productId),
+        transactionState: SKPaymentTransactionStateWrapper.restored,
+        transactionTimeStamp: 123123.121,
+        transactionIdentifier: transactionId,
+        error: null,
+        originalTransaction: null);
+  }
+
   Future<dynamic> onMethodCall(MethodCall call) {
     switch (call.method) {
       case '-[SKPaymentQueue canMakePayments:]':
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart
index 8ac4b04..12595bb 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart
+++ b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart
@@ -78,14 +78,18 @@
 
   group('restore purchases', () {
     test('should emit restored transactions on purchase stream', () async {
+      fakeStoreKitPlatform.transactions.insert(
+          0, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT1'));
+      fakeStoreKitPlatform.transactions.insert(
+          1, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT2'));
       Completer completer = Completer();
       Stream<List<PurchaseDetails>> stream = iapStoreKitPlatform.purchaseStream;
 
       late StreamSubscription subscription;
       subscription = stream.listen((purchaseDetailsList) {
         if (purchaseDetailsList.first.status == PurchaseStatus.restored) {
-          completer.complete(purchaseDetailsList);
           subscription.cancel();
+          completer.complete(purchaseDetailsList);
         }
       });
 
@@ -109,15 +113,37 @@
       }
     });
 
-    test('should not block transaction updates', () async {
-      fakeStoreKitPlatform.transactions.insert(
-          0, fakeStoreKitPlatform.createPurchasedTransaction('foo', 'bar'));
+    test(
+        'should emit empty transaction list on purchase stream when there is nothing to restore',
+        () async {
+      fakeStoreKitPlatform.testRestoredTransactionsNull = true;
       Completer completer = Completer();
       Stream<List<PurchaseDetails>> stream = iapStoreKitPlatform.purchaseStream;
 
       late StreamSubscription subscription;
       subscription = stream.listen((purchaseDetailsList) {
-        if (purchaseDetailsList.first.status == PurchaseStatus.purchased) {
+        expect(purchaseDetailsList.isEmpty, true);
+        subscription.cancel();
+        completer.complete();
+      });
+
+      await iapStoreKitPlatform.restorePurchases();
+      await completer.future;
+    });
+
+    test('should not block transaction updates', () async {
+      fakeStoreKitPlatform.transactions.insert(
+          0, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT1'));
+      fakeStoreKitPlatform.transactions.insert(
+          1, fakeStoreKitPlatform.createPurchasedTransaction('foo', 'bar'));
+      fakeStoreKitPlatform.transactions.insert(
+          2, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT2'));
+      Completer completer = Completer();
+      Stream<List<PurchaseDetails>> stream = iapStoreKitPlatform.purchaseStream;
+
+      late StreamSubscription subscription;
+      subscription = stream.listen((purchaseDetailsList) {
+        if (purchaseDetailsList[1].status == PurchaseStatus.purchased) {
           completer.complete(purchaseDetailsList);
           subscription.cancel();
         }
@@ -145,8 +171,54 @@
       }
     });
 
+    test(
+        'should emit empty transaction if transactions array does not contain a transaction with PurchaseStatus.restored status.',
+        () async {
+      fakeStoreKitPlatform.transactions.insert(
+          0, fakeStoreKitPlatform.createPurchasedTransaction('foo', 'bar'));
+      Completer completer = Completer();
+      Stream<List<PurchaseDetails>> stream = iapStoreKitPlatform.purchaseStream;
+      List<List<PurchaseDetails>> purchaseDetails = [];
+
+      late StreamSubscription subscription;
+      subscription = stream.listen((purchaseDetailsList) {
+        purchaseDetails.add(purchaseDetailsList);
+
+        if (purchaseDetails.length == 2) {
+          completer.complete(purchaseDetails);
+          subscription.cancel();
+        }
+      });
+      await iapStoreKitPlatform.restorePurchases();
+      final details = await completer.future;
+      expect(details.length, 2);
+      expect(details[0], []);
+      for (int i = 0; i < fakeStoreKitPlatform.transactions.length; i++) {
+        SKPaymentTransactionWrapper expected =
+            fakeStoreKitPlatform.transactions[i];
+        PurchaseDetails actual = details[1][i];
+
+        expect(actual.purchaseID, expected.transactionIdentifier);
+        expect(actual.verificationData, isNotNull);
+        expect(
+          actual.status,
+          SKTransactionStatusConverter()
+              .toPurchaseStatus(expected.transactionState, expected.error),
+        );
+        expect(actual.verificationData.localVerificationData,
+            fakeStoreKitPlatform.receiptData);
+        expect(actual.verificationData.serverVerificationData,
+            fakeStoreKitPlatform.receiptData);
+        expect(actual.pendingCompletePurchase, true);
+      }
+    });
+
     test('receipt error should populate null to verificationData.data',
         () async {
+      fakeStoreKitPlatform.transactions.insert(
+          0, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT1'));
+      fakeStoreKitPlatform.transactions.insert(
+          1, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT2'));
       fakeStoreKitPlatform.receiptData = null;
       Completer completer = Completer();
       Stream<List<PurchaseDetails>> stream = iapStoreKitPlatform.purchaseStream;