[in_app_purchase] Implements transaction caching for StoreKit (#4985)

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 c5df521..087c6e3 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+3
+
+* Implements transaction caching for StoreKit ensuring transactions are delivered to the Flutter client.
+
 ## 0.3.0+2
 
 * Internal code cleanup for stricter analysis options.
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj
index a880501..3977d54 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
 	archiveVersion = 1;
 	classes = {
 	};
-	objectVersion = 46;
+	objectVersion = 50;
 	objects = {
 
 /* Begin PBXBuildFile section */
@@ -22,6 +22,7 @@
 		A5279298219369C600FF69E6 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5279297219369C600FF69E6 /* StoreKit.framework */; };
 		A59001A721E69658004A3E5E /* InAppPurchasePluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A59001A621E69658004A3E5E /* InAppPurchasePluginTests.m */; };
 		F67646F82681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F67646F72681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m */; };
+		F6995BDD27CF73000050EA78 /* FIATransactionCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F6995BDC27CF73000050EA78 /* FIATransactionCacheTests.m */; };
 		F78AF3142342BC89008449C7 /* PaymentQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F78AF3132342BC89008449C7 /* PaymentQueueTests.m */; };
 /* End PBXBuildFile section */
 
@@ -78,6 +79,7 @@
 		A59001A821E69658004A3E5E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		E4F9651425A612301059769C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
 		F67646F72681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIAPPaymentQueueDeleteTests.m; sourceTree = "<group>"; };
+		F6995BDC27CF73000050EA78 /* FIATransactionCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIATransactionCacheTests.m; sourceTree = "<group>"; };
 		F6E5D5F926131C4800C68BED /* Configuration.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Configuration.storekit; sourceTree = "<group>"; };
 		F78AF3132342BC89008449C7 /* PaymentQueueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PaymentQueueTests.m; sourceTree = "<group>"; };
 /* End PBXFileReference section */
@@ -190,6 +192,7 @@
 				F78AF3132342BC89008449C7 /* PaymentQueueTests.m */,
 				688DE35021F2A5A100EA2684 /* TranslatorTests.m */,
 				F67646F72681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m */,
+				F6995BDC27CF73000050EA78 /* FIATransactionCacheTests.m */,
 			);
 			path = RunnerTests;
 			sourceTree = "<group>";
@@ -254,7 +257,7 @@
 			isa = PBXProject;
 			attributes = {
 				DefaultBuildSystemTypeForWorkspace = Original;
-				LastUpgradeCheck = 1100;
+				LastUpgradeCheck = 1300;
 				ORGANIZATIONNAME = "The Flutter Authors";
 				TargetAttributes = {
 					97C146ED1CF9000F007C117D = {
@@ -406,6 +409,7 @@
 				F67646F82681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m in Sources */,
 				6896B34621E9363700D37AEF /* ProductRequestHandlerTests.m in Sources */,
 				688DE35121F2A5A100EA2684 /* TranslatorTests.m in Sources */,
+				F6995BDD27CF73000050EA78 /* FIATransactionCacheTests.m in Sources */,
 				A59001A721E69658004A3E5E /* InAppPurchasePluginTests.m in Sources */,
 				6896B34C21EEB4B800D37AEF /* Stubs.m in Sources */,
 			);
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index 3bd47ec..a8adf88 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1100"
+   LastUpgradeVersion = "1300"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIATransactionCacheTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIATransactionCacheTests.m
new file mode 100644
index 0000000..1ba0aea
--- /dev/null
+++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIATransactionCacheTests.m
@@ -0,0 +1,63 @@
+// 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 <XCTest/XCTest.h>
+
+@import in_app_purchase_storekit;
+
+@interface FIATransactionCacheTests : XCTestCase
+
+@end
+
+@implementation FIATransactionCacheTests
+
+- (void)testAddObjectsForNewKey {
+  NSArray *dummyArray = @[ @1, @2, @3 ];
+  FIATransactionCache *cache = [[FIATransactionCache alloc] init];
+  [cache addObjects:dummyArray forKey:TransactionCacheKeyUpdatedTransactions];
+
+  XCTAssertEqual(dummyArray, [cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]);
+}
+
+- (void)testAddObjectsForExistingKey {
+  NSArray *dummyArray = @[ @1, @2, @3 ];
+  FIATransactionCache *cache = [[FIATransactionCache alloc] init];
+  [cache addObjects:dummyArray forKey:TransactionCacheKeyUpdatedTransactions];
+
+  XCTAssertEqual(dummyArray, [cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]);
+
+  [cache addObjects:@[ @4, @5, @6 ] forKey:TransactionCacheKeyUpdatedTransactions];
+
+  NSArray *expected = @[ @1, @2, @3, @4, @5, @6 ];
+  XCTAssertEqualObjects(expected, [cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]);
+}
+
+- (void)testGetObjectsForNonExistingKey {
+  FIATransactionCache *cache = [[FIATransactionCache alloc] init];
+  XCTAssertNil([cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]);
+}
+
+- (void)testClear {
+  NSArray *fakeUpdatedTransactions = @[ @1, @2, @3 ];
+  NSArray *fakeRemovedTransactions = @[ @"Remove 1", @"Remove 2", @"Remove 3" ];
+  NSArray *fakeUpdatedDownloads = @[ @"Download 1", @"Download 2" ];
+  FIATransactionCache *cache = [[FIATransactionCache alloc] init];
+  [cache addObjects:fakeUpdatedTransactions forKey:TransactionCacheKeyUpdatedTransactions];
+  [cache addObjects:fakeRemovedTransactions forKey:TransactionCacheKeyRemovedTransactions];
+  [cache addObjects:fakeUpdatedDownloads forKey:TransactionCacheKeyUpdatedDownloads];
+
+  XCTAssertEqual(fakeUpdatedTransactions,
+                 [cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]);
+  XCTAssertEqual(fakeRemovedTransactions,
+                 [cache getObjectsForKey:TransactionCacheKeyRemovedTransactions]);
+  XCTAssertEqual(fakeUpdatedDownloads,
+                 [cache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]);
+
+  [cache clear];
+
+  XCTAssertNil([cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]);
+  XCTAssertNil([cache getObjectsForKey:TransactionCacheKeyRemovedTransactions]);
+  XCTAssertNil([cache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]);
+}
+@end
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.m
index fcb2c94..c89589c 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.m
+++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.m
@@ -74,46 +74,63 @@
   XCTAssertTrue([resultArray.firstObject[@"productIdentifier"] isEqualToString:@"123"]);
 }
 
-- (void)testAddPaymentFailure {
+- (void)testAddPaymentShouldReturnFlutterErrorWhenArgumentsAreInvalid {
   XCTestExpectation *expectation =
-      [self expectationWithDescription:@"result should return failed state"];
+      [self expectationWithDescription:
+                @"Result should contain a FlutterError when invalid parameters are passed in."];
+  NSString *argument = @"Invalid argument";
   FlutterMethodCall *call =
       [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]"
-                                        arguments:@{
-                                          @"productIdentifier" : @"123",
-                                          @"quantity" : @(1),
-                                          @"simulatesAskToBuyInSandbox" : @YES,
-                                        }];
-  SKPaymentQueueStub *queue = [SKPaymentQueueStub new];
-  queue.testState = SKPaymentTransactionStateFailed;
-  __block SKPaymentTransaction *transactionForUpdateBlock;
-  self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue
-      transactionsUpdated:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) {
-        SKPaymentTransaction *transaction = transactions[0];
-        if (transaction.transactionState == SKPaymentTransactionStateFailed) {
-          transactionForUpdateBlock = transaction;
-          [expectation fulfill];
-        }
-      }
-      transactionRemoved:nil
-      restoreTransactionFailed:nil
-      restoreCompletedTransactionsFinished:nil
-      shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) {
-        return YES;
-      }
-      updatedDownloads:nil];
-  [queue addTransactionObserver:self.plugin.paymentQueueHandler];
+                                        arguments:argument];
+  [self.plugin handleMethodCall:call
+                         result:^(id _Nullable result) {
+                           FlutterError *error = result;
+                           XCTAssertEqualObjects(@"storekit_invalid_argument", error.code);
+                           XCTAssertEqualObjects(@"Argument type of addPayment is not a Dictionary",
+                                                 error.message);
+                           XCTAssertEqualObjects(argument, error.details);
+                           [expectation fulfill];
+                         }];
+
+  [self waitForExpectations:@[ expectation ] timeout:5];
+}
+
+- (void)testAddPaymentShouldReturnFlutterErrorWhenPaymentFails {
+  NSDictionary *arguments = @{
+    @"productIdentifier" : @"123",
+    @"quantity" : @(1),
+    @"simulatesAskToBuyInSandbox" : @YES,
+  };
+  XCTestExpectation *expectation =
+      [self expectationWithDescription:@"Result should return failed state."];
+  FlutterMethodCall *call =
+      [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]"
+                                        arguments:arguments];
+
+  FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class);
+  OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(NO);
+  self.plugin.paymentQueueHandler = mockHandler;
 
   [self.plugin handleMethodCall:call
-                         result:^(id r){
+                         result:^(id _Nullable result) {
+                           FlutterError *error = result;
+                           XCTAssertEqualObjects(@"storekit_duplicate_product_object", error.code);
+                           XCTAssertEqualObjects(
+                               @"There is a pending transaction for the same product identifier. "
+                               @"Please either wait for it to be finished or finish it manually "
+                               @"using `completePurchase` to avoid edge cases.",
+                               error.message);
+                           XCTAssertEqualObjects(arguments, error.details);
+                           [expectation fulfill];
                          }];
+
   [self waitForExpectations:@[ expectation ] timeout:5];
-  XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStateFailed);
+  OCMVerify(times(1), [mockHandler addPayment:[OCMArg any]]);
 }
 
 - (void)testAddPaymentSuccessWithoutPaymentDiscount {
   XCTestExpectation *expectation =
-      [self expectationWithDescription:@"result should return success state"];
+      [self expectationWithDescription:@"Result should return success state"];
   FlutterMethodCall *call =
       [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]"
                                         arguments:@{
@@ -121,38 +138,20 @@
                                           @"quantity" : @(1),
                                           @"simulatesAskToBuyInSandbox" : @YES,
                                         }];
-  SKPaymentQueueStub *queue = [SKPaymentQueueStub new];
-  queue.testState = SKPaymentTransactionStatePurchased;
-  __block SKPaymentTransaction *transactionForUpdateBlock;
-  self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue
-      transactionsUpdated:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) {
-        SKPaymentTransaction *transaction = transactions[0];
-        if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
-          transactionForUpdateBlock = transaction;
-          if (@available(iOS 12.2, *)) {
-            XCTAssertNil(transaction.payment.paymentDiscount);
-          }
-          [expectation fulfill];
-        }
-      }
-      transactionRemoved:nil
-      restoreTransactionFailed:nil
-      restoreCompletedTransactionsFinished:nil
-      shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) {
-        return YES;
-      }
-      updatedDownloads:nil];
-  [queue addTransactionObserver:self.plugin.paymentQueueHandler];
+  FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class);
+  OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(YES);
+  self.plugin.paymentQueueHandler = mockHandler;
   [self.plugin handleMethodCall:call
-                         result:^(id r){
+                         result:^(id _Nullable result) {
+                           XCTAssertNil(result);
+                           [expectation fulfill];
                          }];
   [self waitForExpectations:@[ expectation ] timeout:5];
-  XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased);
 }
 
 - (void)testAddPaymentSuccessWithPaymentDiscount {
   XCTestExpectation *expectation =
-      [self expectationWithDescription:@"result should return success state"];
+      [self expectationWithDescription:@"Result should return success state"];
   FlutterMethodCall *call =
       [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]"
                                         arguments:@{
@@ -167,81 +166,86 @@
                                             @"timestamp" : @(1635847102),
                                           }
                                         }];
-  SKPaymentQueueStub *queue = [SKPaymentQueueStub new];
-  queue.testState = SKPaymentTransactionStatePurchased;
-  __block SKPaymentTransaction *transactionForUpdateBlock;
-  self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue
-      transactionsUpdated:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) {
-        SKPaymentTransaction *transaction = transactions[0];
-        if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
-          transactionForUpdateBlock = transaction;
-          if (@available(iOS 12.2, *)) {
-            SKPaymentDiscount *paymentDiscount = transaction.payment.paymentDiscount;
-            XCTAssertEqual(paymentDiscount.identifier, @"test_identifier");
-            XCTAssertEqual(paymentDiscount.keyIdentifier, @"test_key_identifier");
-            XCTAssertEqualObjects(
-                paymentDiscount.nonce,
-                [[NSUUID alloc] initWithUUIDString:@"4a11a9cc-3bc3-11ec-8d3d-0242ac130003"]);
-            XCTAssertEqual(paymentDiscount.signature, @"test_signature");
-            XCTAssertEqual(paymentDiscount.timestamp, @(1635847102));
-          }
-          [expectation fulfill];
-        }
-      }
-      transactionRemoved:nil
-      restoreTransactionFailed:nil
-      restoreCompletedTransactionsFinished:nil
-      shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) {
-        return YES;
-      }
-      updatedDownloads:nil];
-  [queue addTransactionObserver:self.plugin.paymentQueueHandler];
+
+  FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class);
+  OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(YES);
+  self.plugin.paymentQueueHandler = mockHandler;
   [self.plugin handleMethodCall:call
-                         result:^(id r){
+                         result:^(id _Nullable result) {
+                           XCTAssertNil(result);
+                           [expectation fulfill];
                          }];
   [self waitForExpectations:@[ expectation ] timeout:5];
-  XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased);
+  OCMVerify(
+      times(1),
+      [mockHandler
+          addPayment:[OCMArg checkWithBlock:^BOOL(id obj) {
+            SKPayment *payment = obj;
+            if (@available(iOS 12.2, *)) {
+              SKPaymentDiscount *discount = payment.paymentDiscount;
+
+              return [discount.identifier isEqual:@"test_identifier"] &&
+                     [discount.keyIdentifier isEqual:@"test_key_identifier"] &&
+                     [discount.nonce
+                         isEqual:[[NSUUID alloc]
+                                     initWithUUIDString:@"4a11a9cc-3bc3-11ec-8d3d-0242ac130003"]] &&
+                     [discount.signature isEqual:@"test_signature"] &&
+                     [discount.timestamp isEqual:@(1635847102)];
+            }
+
+            return YES;
+          }]]);
 }
 
 - (void)testAddPaymentFailureWithInvalidPaymentDiscount {
-  XCTestExpectation *expectation =
-      [self expectationWithDescription:@"result should return success state"];
-  FlutterMethodCall *call =
-      [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]"
-                                        arguments:@{
-                                          @"productIdentifier" : @"123",
-                                          @"quantity" : @(1),
-                                          @"simulatesAskToBuyInSandbox" : @YES,
-                                          @"paymentDiscount" : @{
-                                            @"keyIdentifier" : @"test_key_identifier",
-                                            @"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003",
-                                            @"signature" : @"test_signature",
-                                            @"timestamp" : @(1635847102),
-                                          }
-                                        }];
+  // Support for payment discount is only available on iOS 12.2 and higher.
+  if (@available(iOS 12.2, *)) {
+    XCTestExpectation *expectation =
+        [self expectationWithDescription:@"Result should return success state"];
+    NSDictionary *arguments = @{
+      @"productIdentifier" : @"123",
+      @"quantity" : @(1),
+      @"simulatesAskToBuyInSandbox" : @YES,
+      @"paymentDiscount" : @{
+        @"keyIdentifier" : @"test_key_identifier",
+        @"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003",
+        @"signature" : @"test_signature",
+        @"timestamp" : @(1635847102),
+      }
+    };
+    FlutterMethodCall *call =
+        [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]"
+                                          arguments:arguments];
 
-  [self.plugin
-      handleMethodCall:call
-                result:^(id r) {
-                  XCTAssertTrue([r isKindOfClass:FlutterError.class]);
-                  FlutterError *result = r;
-                  XCTAssertEqualObjects(result.code, @"storekit_invalid_payment_discount_object");
-                  XCTAssertEqualObjects(result.message,
-                                        @"You have requested a payment and specified a payment "
-                                        @"discount with invalid properties. When specifying a "
-                                        @"payment discount the 'identifier' field is mandatory.");
-                  XCTAssertEqualObjects(result.details, call.arguments);
-                  [expectation fulfill];
-                }];
+    FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class);
+    id translator = OCMClassMock(FIAObjectTranslator.class);
 
-  [self waitForExpectations:@[ expectation ] timeout:5];
+    NSString *error = @"Some error occurred";
+    OCMStub(ClassMethod([translator
+                getSKPaymentDiscountFromMap:[OCMArg any]
+                                  withError:(NSString __autoreleasing **)[OCMArg setTo:error]]))
+        .andReturn(nil);
+    self.plugin.paymentQueueHandler = mockHandler;
+    [self.plugin
+        handleMethodCall:call
+                  result:^(id _Nullable result) {
+                    FlutterError *error = result;
+                    XCTAssertEqualObjects(@"storekit_invalid_payment_discount_object", error.code);
+                    XCTAssertEqualObjects(
+                        @"You have requested a payment and specified a "
+                        @"payment discount with invalid properties. Some error occurred",
+                        error.message);
+                    XCTAssertEqualObjects(arguments, error.details);
+                    [expectation fulfill];
+                  }];
+    [self waitForExpectations:@[ expectation ] timeout:5];
+    OCMVerify(never(), [mockHandler addPayment:[OCMArg any]]);
+  }
 }
 
 - (void)testAddPaymentWithNullSandboxArgument {
   XCTestExpectation *expectation =
       [self expectationWithDescription:@"result should return success state"];
-  XCTestExpectation *simulatesAskToBuyInSandboxExpectation =
-      [self expectationWithDescription:@"payment isn't simulatesAskToBuyInSandbox"];
   FlutterMethodCall *call =
       [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]"
                                         arguments:@{
@@ -249,33 +253,19 @@
                                           @"quantity" : @(1),
                                           @"simulatesAskToBuyInSandbox" : [NSNull null],
                                         }];
-  SKPaymentQueueStub *queue = [SKPaymentQueueStub new];
-  queue.testState = SKPaymentTransactionStatePurchased;
-  __block SKPaymentTransaction *transactionForUpdateBlock;
-  self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue
-      transactionsUpdated:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) {
-        SKPaymentTransaction *transaction = transactions[0];
-        if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
-          transactionForUpdateBlock = transaction;
-          [expectation fulfill];
-        }
-        if (!transaction.payment.simulatesAskToBuyInSandbox) {
-          [simulatesAskToBuyInSandboxExpectation fulfill];
-        }
-      }
-      transactionRemoved:nil
-      restoreTransactionFailed:nil
-      restoreCompletedTransactionsFinished:nil
-      shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) {
-        return YES;
-      }
-      updatedDownloads:nil];
-  [queue addTransactionObserver:self.plugin.paymentQueueHandler];
+  FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class);
+  OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(YES);
+  self.plugin.paymentQueueHandler = mockHandler;
   [self.plugin handleMethodCall:call
-                         result:^(id r){
+                         result:^(id _Nullable result) {
+                           XCTAssertNil(result);
+                           [expectation fulfill];
                          }];
-  [self waitForExpectations:@[ expectation, simulatesAskToBuyInSandboxExpectation ] timeout:5];
-  XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased);
+  [self waitForExpectations:@[ expectation ] timeout:5];
+  OCMVerify(times(1), [mockHandler addPayment:[OCMArg checkWithBlock:^BOOL(id obj) {
+                                     SKPayment *payment = obj;
+                                     return !payment.simulatesAskToBuyInSandbox;
+                                   }]]);
 }
 
 - (void)testRestoreTransactions {
@@ -297,7 +287,8 @@
         [expectation fulfill];
       }
       shouldAddStorePayment:nil
-      updatedDownloads:nil];
+      updatedDownloads:nil
+      transactionCache:OCMClassMock(FIATransactionCache.class)];
   [queue addTransactionObserver:self.plugin.paymentQueueHandler];
   [self.plugin handleMethodCall:call
                          result:^(id r){
@@ -393,13 +384,15 @@
       initWithMap:transactionMap] ]);
 
   __block NSArray *resultArray;
-  self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:mockQueue
-                                                              transactionsUpdated:nil
-                                                               transactionRemoved:nil
-                                                         restoreTransactionFailed:nil
-                                             restoreCompletedTransactionsFinished:nil
-                                                            shouldAddStorePayment:nil
-                                                                 updatedDownloads:nil];
+  self.plugin.paymentQueueHandler =
+      [[FIAPaymentQueueHandler alloc] initWithQueue:mockQueue
+                                transactionsUpdated:nil
+                                 transactionRemoved:nil
+                           restoreTransactionFailed:nil
+               restoreCompletedTransactionsFinished:nil
+                              shouldAddStorePayment:nil
+                                   updatedDownloads:nil
+                                   transactionCache:OCMClassMock(FIATransactionCache.class)];
   [self.plugin handleMethodCall:call
                          result:^(id r) {
                            resultArray = r;
@@ -409,46 +402,40 @@
   XCTAssertEqualObjects(resultArray, @[ transactionMap ]);
 }
 
-- (void)testStartAndStopObservingPaymentQueue {
+- (void)testStartObservingPaymentQueue {
+  XCTestExpectation *expectation =
+      [self expectationWithDescription:@"Should return success result"];
   FlutterMethodCall *startCall = [FlutterMethodCall
       methodCallWithMethodName:@"-[SKPaymentQueue startObservingTransactionQueue]"
                      arguments:nil];
+  FIAPaymentQueueHandler *mockHandler = OCMClassMock([FIAPaymentQueueHandler class]);
+  self.plugin.paymentQueueHandler = mockHandler;
+  [self.plugin handleMethodCall:startCall
+                         result:^(id _Nullable result) {
+                           XCTAssertNil(result);
+                           [expectation fulfill];
+                         }];
+
+  [self waitForExpectations:@[ expectation ] timeout:5];
+  OCMVerify(times(1), [mockHandler startObservingPaymentQueue]);
+}
+
+- (void)testStopObservingPaymentQueue {
+  XCTestExpectation *expectation =
+      [self expectationWithDescription:@"Should return success result"];
   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
+  FIAPaymentQueueHandler *mockHandler = OCMClassMock([FIAPaymentQueueHandler class]);
+  self.plugin.paymentQueueHandler = mockHandler;
   [self.plugin handleMethodCall:stopCall
-                         result:^(id r){
+                         result:^(id _Nullable result) {
+                           XCTAssertNil(result);
+                           [expectation fulfill];
                          }];
 
-  // No observer should be set
-  XCTAssertNil(queue.observer);
+  [self waitForExpectations:@[ expectation ] timeout:5];
+  OCMVerify(times(1), [mockHandler stopObservingPaymentQueue]);
 }
 
 - (void)testRegisterPaymentQueueDelegate {
@@ -464,7 +451,8 @@
                              restoreTransactionFailed:nil
                  restoreCompletedTransactionsFinished:nil
                                 shouldAddStorePayment:nil
-                                     updatedDownloads:nil];
+                                     updatedDownloads:nil
+                                     transactionCache:OCMClassMock(FIATransactionCache.class)];
 
     // Verify the delegate is nil before we register one.
     XCTAssertNil(self.plugin.paymentQueueHandler.delegate);
@@ -491,7 +479,8 @@
                              restoreTransactionFailed:nil
                  restoreCompletedTransactionsFinished:nil
                                 shouldAddStorePayment:nil
-                                     updatedDownloads:nil];
+                                     updatedDownloads:nil
+                                     transactionCache:OCMClassMock(FIATransactionCache.class)];
     self.plugin.paymentQueueHandler.delegate = OCMProtocolMock(@protocol(SKPaymentQueueDelegate));
 
     // Verify the delegate is not nil before removing it.
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/PaymentQueueTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/PaymentQueueTests.m
index e267be1..2f8d585 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/PaymentQueueTests.m
+++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/PaymentQueueTests.m
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#import <OCMock/OCMock.h>
 #import <XCTest/XCTest.h>
 #import "Stubs.h"
 
@@ -59,10 +60,11 @@
       shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) {
         return YES;
       }
-      updatedDownloads:nil];
-  [queue addTransactionObserver:handler];
+      updatedDownloads:nil
+      transactionCache:OCMClassMock(FIATransactionCache.class)];
   SKPayment *payment =
       [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]];
+  [handler startObservingPaymentQueue];
   [handler addPayment:payment];
   [self waitForExpectations:@[ expectation ] timeout:5];
   XCTAssertEqual(tran.transactionState, SKPaymentTransactionStatePurchased);
@@ -87,10 +89,12 @@
       shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) {
         return YES;
       }
-      updatedDownloads:nil];
-  [queue addTransactionObserver:handler];
+      updatedDownloads:nil
+      transactionCache:OCMClassMock(FIATransactionCache.class)];
+
   SKPayment *payment =
       [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]];
+  [handler startObservingPaymentQueue];
   [handler addPayment:payment];
   [self waitForExpectations:@[ expectation ] timeout:5];
   XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateFailed);
@@ -115,10 +119,12 @@
       shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) {
         return YES;
       }
-      updatedDownloads:nil];
-  [queue addTransactionObserver:handler];
+      updatedDownloads:nil
+      transactionCache:OCMClassMock(FIATransactionCache.class)];
+
   SKPayment *payment =
       [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]];
+  [handler startObservingPaymentQueue];
   [handler addPayment:payment];
   [self waitForExpectations:@[ expectation ] timeout:5];
   XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateRestored);
@@ -143,10 +149,12 @@
       shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) {
         return YES;
       }
-      updatedDownloads:nil];
-  [queue addTransactionObserver:handler];
+      updatedDownloads:nil
+      transactionCache:OCMClassMock(FIATransactionCache.class)];
+
   SKPayment *payment =
       [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]];
+  [handler startObservingPaymentQueue];
   [handler addPayment:payment];
   [self waitForExpectations:@[ expectation ] timeout:5];
   XCTAssertEqual(tran.transactionState, SKPaymentTransactionStatePurchasing);
@@ -171,10 +179,11 @@
       shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) {
         return YES;
       }
-      updatedDownloads:nil];
-  [queue addTransactionObserver:handler];
+      updatedDownloads:nil
+      transactionCache:OCMClassMock(FIATransactionCache.class)];
   SKPayment *payment =
       [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]];
+  [handler startObservingPaymentQueue];
   [handler addPayment:payment];
   [self waitForExpectations:@[ expectation ] timeout:5];
   XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateDeferred);
@@ -201,12 +210,211 @@
       shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) {
         return YES;
       }
-      updatedDownloads:nil];
-  [queue addTransactionObserver:handler];
+      updatedDownloads:nil
+      transactionCache:OCMClassMock(FIATransactionCache.class)];
   SKPayment *payment =
       [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]];
+  [handler startObservingPaymentQueue];
   [handler addPayment:payment];
   [self waitForExpectations:@[ expectation ] timeout:5];
 }
 
+- (void)testStartObservingPaymentQueueShouldNotProcessTransactionsWhenCacheIsEmpty {
+  FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class);
+  FIAPaymentQueueHandler *handler =
+      [[FIAPaymentQueueHandler alloc] initWithQueue:[[SKPaymentQueueStub alloc] init]
+          transactionsUpdated:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) {
+            XCTFail("transactionsUpdated callback should not be called when cache is empty.");
+          }
+          transactionRemoved:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) {
+            XCTFail("transactionRemoved callback should not be called when cache is empty.");
+          }
+          restoreTransactionFailed:nil
+          restoreCompletedTransactionsFinished:nil
+          shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) {
+            return YES;
+          }
+          updatedDownloads:^(NSArray<SKDownload *> *_Nonnull downloads) {
+            XCTFail("updatedDownloads callback should not be called when cache is empty.");
+          }
+          transactionCache:mockCache];
+
+  [handler startObservingPaymentQueue];
+
+  OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]);
+  OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]);
+  OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]);
+}
+
+- (void)
+    testStartObservingPaymentQueueShouldNotProcessTransactionsWhenCacheContainsEmptyTransactionArrays {
+  FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class);
+  FIAPaymentQueueHandler *handler =
+      [[FIAPaymentQueueHandler alloc] initWithQueue:[[SKPaymentQueueStub alloc] init]
+          transactionsUpdated:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) {
+            XCTFail("transactionsUpdated callback should not be called when cache is empty.");
+          }
+          transactionRemoved:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) {
+            XCTFail("transactionRemoved callback should not be called when cache is empty.");
+          }
+          restoreTransactionFailed:nil
+          restoreCompletedTransactionsFinished:nil
+          shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) {
+            return YES;
+          }
+          updatedDownloads:^(NSArray<SKDownload *> *_Nonnull downloads) {
+            XCTFail("updatedDownloads callback should not be called when cache is empty.");
+          }
+          transactionCache:mockCache];
+
+  OCMStub([mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]).andReturn(@[]);
+  OCMStub([mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]).andReturn(@[]);
+  OCMStub([mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]).andReturn(@[]);
+
+  [handler startObservingPaymentQueue];
+
+  OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]);
+  OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]);
+  OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]);
+}
+
+- (void)testStartObservingPaymentQueueShouldProcessTransactionsForItemsInCache {
+  XCTestExpectation *updateTransactionsExpectation =
+      [self expectationWithDescription:
+                @"transactionsUpdated callback should be called with one transaction."];
+  XCTestExpectation *removeTransactionsExpectation =
+      [self expectationWithDescription:
+                @"transactionsRemoved callback should be called with one transaction."];
+  XCTestExpectation *updateDownloadsExpectation =
+      [self expectationWithDescription:
+                @"downloadsUpdated callback should be called with one transaction."];
+  SKPaymentTransaction *mockTransaction = OCMClassMock(SKPaymentTransaction.class);
+  SKDownload *mockDownload = OCMClassMock(SKDownload.class);
+  FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class);
+  FIAPaymentQueueHandler *handler =
+      [[FIAPaymentQueueHandler alloc] initWithQueue:[[SKPaymentQueueStub alloc] init]
+          transactionsUpdated:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) {
+            XCTAssertEqualObjects(transactions, @[ mockTransaction ]);
+            [updateTransactionsExpectation fulfill];
+          }
+          transactionRemoved:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) {
+            XCTAssertEqualObjects(transactions, @[ mockTransaction ]);
+            [removeTransactionsExpectation fulfill];
+          }
+          restoreTransactionFailed:nil
+          restoreCompletedTransactionsFinished:nil
+          shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) {
+            return YES;
+          }
+          updatedDownloads:^(NSArray<SKDownload *> *_Nonnull downloads) {
+            XCTAssertEqualObjects(downloads, @[ mockDownload ]);
+            [updateDownloadsExpectation fulfill];
+          }
+          transactionCache:mockCache];
+
+  OCMStub([mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]).andReturn(@[
+    mockTransaction
+  ]);
+  OCMStub([mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]).andReturn(@[
+    mockDownload
+  ]);
+  OCMStub([mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]).andReturn(@[
+    mockTransaction
+  ]);
+
+  [handler startObservingPaymentQueue];
+
+  [self waitForExpectations:@[
+    updateTransactionsExpectation, removeTransactionsExpectation, updateDownloadsExpectation
+  ]
+                    timeout:5];
+  OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]);
+  OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]);
+  OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]);
+  OCMVerify(times(1), [mockCache clear]);
+}
+
+- (void)testTransactionsShouldBeCachedWhenNotObserving {
+  SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init];
+  FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class);
+  FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue
+      transactionsUpdated:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) {
+        XCTFail("transactionsUpdated callback should not be called when cache is empty.");
+      }
+      transactionRemoved:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) {
+        XCTFail("transactionRemoved callback should not be called when cache is empty.");
+      }
+      restoreTransactionFailed:nil
+      restoreCompletedTransactionsFinished:nil
+      shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) {
+        return YES;
+      }
+      updatedDownloads:^(NSArray<SKDownload *> *_Nonnull downloads) {
+        XCTFail("updatedDownloads callback should not be called when cache is empty.");
+      }
+      transactionCache:mockCache];
+
+  SKPayment *payment =
+      [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]];
+  [handler addPayment:payment];
+
+  OCMVerify(times(1), [mockCache addObjects:[OCMArg any]
+                                     forKey:TransactionCacheKeyUpdatedTransactions]);
+  OCMVerify(never(), [mockCache addObjects:[OCMArg any]
+                                    forKey:TransactionCacheKeyUpdatedDownloads]);
+  OCMVerify(never(), [mockCache addObjects:[OCMArg any]
+                                    forKey:TransactionCacheKeyRemovedTransactions]);
+}
+
+- (void)testTransactionsShouldNotBeCachedWhenObserving {
+  XCTestExpectation *updateTransactionsExpectation =
+      [self expectationWithDescription:
+                @"transactionsUpdated callback should be called with one transaction."];
+  XCTestExpectation *removeTransactionsExpectation =
+      [self expectationWithDescription:
+                @"transactionsRemoved callback should be called with one transaction."];
+  XCTestExpectation *updateDownloadsExpectation =
+      [self expectationWithDescription:
+                @"downloadsUpdated callback should be called with one transaction."];
+  SKPaymentTransaction *mockTransaction = OCMClassMock(SKPaymentTransaction.class);
+  SKDownload *mockDownload = OCMClassMock(SKDownload.class);
+  SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init];
+  queue.testState = SKPaymentTransactionStatePurchased;
+  FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class);
+  FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue
+      transactionsUpdated:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) {
+        XCTAssertEqualObjects(transactions, @[ mockTransaction ]);
+        [updateTransactionsExpectation fulfill];
+      }
+      transactionRemoved:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) {
+        XCTAssertEqualObjects(transactions, @[ mockTransaction ]);
+        [removeTransactionsExpectation fulfill];
+      }
+      restoreTransactionFailed:nil
+      restoreCompletedTransactionsFinished:nil
+      shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) {
+        return YES;
+      }
+      updatedDownloads:^(NSArray<SKDownload *> *_Nonnull downloads) {
+        XCTAssertEqualObjects(downloads, @[ mockDownload ]);
+        [updateDownloadsExpectation fulfill];
+      }
+      transactionCache:mockCache];
+
+  [handler startObservingPaymentQueue];
+  [handler paymentQueue:queue updatedTransactions:@[ mockTransaction ]];
+  [handler paymentQueue:queue removedTransactions:@[ mockTransaction ]];
+  [handler paymentQueue:queue updatedDownloads:@[ mockDownload ]];
+
+  [self waitForExpectations:@[
+    updateTransactionsExpectation, removeTransactionsExpectation, updateDownloadsExpectation
+  ]
+                    timeout:5];
+  OCMVerify(never(), [mockCache addObjects:[OCMArg any]
+                                    forKey:TransactionCacheKeyUpdatedTransactions]);
+  OCMVerify(never(), [mockCache addObjects:[OCMArg any]
+                                    forKey:TransactionCacheKeyUpdatedDownloads]);
+  OCMVerify(never(), [mockCache addObjects:[OCMArg any]
+                                    forKey:TransactionCacheKeyRemovedTransactions]);
+}
 @end
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.h
index 8019831..bb074aa 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.h
+++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.h
@@ -4,6 +4,7 @@
 
 #import <Foundation/Foundation.h>
 #import <StoreKit/StoreKit.h>
+#import "FIATransactionCache.h"
 
 @class SKPaymentTransaction;
 
@@ -21,6 +22,27 @@
 @property(NS_NONATOMIC_IOSONLY, weak, nullable) id<SKPaymentQueueDelegate> delegate API_AVAILABLE(
     ios(13.0), macos(10.15), watchos(6.2));
 
+/// Creates a new FIAPaymentQueueHandler initialized with an empty
+/// FIATransactionCache.
+///
+/// @param queue The SKPaymentQueue instance connected to the App Store and
+///              responsible for processing transactions.
+/// @param transactionsUpdated Callback method that is called each time the App
+///                            Store indicates transactions are updated.
+/// @param transactionsRemoved Callback method that is called each time the App
+///                            Store indicates transactions are removed.
+/// @param restoreTransactionFailed Callback method that is called each time
+///                                 the App Store indicates transactions failed
+///                                 to restore.
+/// @param restoreCompletedTransactionsFinished Callback method that is called
+///                                             each time the App Store
+///                                             indicates restoring of
+///                                             transactions has finished.
+/// @param shouldAddStorePayment Callback method that is called each time an
+///                              in-app purchase has been initiated from the
+///                              App Store.
+/// @param updatedDownloads Callback method that is called each time the App
+///                         Store indicates downloads are updated.
 - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue
                      transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated
                       transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved
@@ -28,7 +50,57 @@
     restoreCompletedTransactionsFinished:
         (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished
                    shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment
-                        updatedDownloads:(nullable UpdatedDownloads)updatedDownloads;
+                        updatedDownloads:(nullable UpdatedDownloads)updatedDownloads
+    DEPRECATED_MSG_ATTRIBUTE(
+        "Use the "
+        "'initWithQueue:transactionsUpdated:transactionsRemoved:restoreTransactionsFinished:"
+        "shouldAddStorePayment:updatedDownloads:transactionCache:' message instead.");
+
+/// Creates a new FIAPaymentQueueHandler.
+///
+/// The "transactionsUpdated", "transactionsRemoved" and "updatedDownloads"
+/// callbacks are only called while actively observing transactions. To start
+/// observing transactions send the "startObservingPaymentQueue" message.
+/// Sending the "stopObservingPaymentQueue" message will stop actively
+/// observing transactions. When transactions are not observed they are cached
+/// to the "transactionCache" and will be delivered via the
+/// "transactionsUpdated", "transactionsRemoved" and "updatedDownloads"
+/// callbacks as soon as the "startObservingPaymentQueue" message arrives.
+///
+/// Note: cached transactions that are not processed when the application is
+/// killed will be delivered again by the App Store as soon as the application
+/// starts again.
+///
+/// @param queue The SKPaymentQueue instance connected to the App Store and
+///              responsible for processing transactions.
+/// @param transactionsUpdated Callback method that is called each time the App
+///                            Store indicates transactions are updated.
+/// @param transactionsRemoved Callback method that is called each time the App
+///                            Store indicates transactions are removed.
+/// @param restoreTransactionFailed Callback method that is called each time
+///                                 the App Store indicates transactions failed
+///                                 to restore.
+/// @param restoreCompletedTransactionsFinished Callback method that is called
+///                                             each time the App Store
+///                                             indicates restoring of
+///                                             transactions has finished.
+/// @param shouldAddStorePayment Callback method that is called each time an
+///                              in-app purchase has been initiated from the
+///                              App Store.
+/// @param updatedDownloads Callback method that is called each time the App
+///                         Store indicates downloads are updated.
+/// @param transactionCache An empty [FIATransactionCache] instance that is
+///                         responsible for keeping track of transactions that
+///                         arrive when not actively observing transactions.
+- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue
+                     transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated
+                      transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved
+                restoreTransactionFailed:(nullable RestoreTransactionFailed)restoreTransactionFailed
+    restoreCompletedTransactionsFinished:
+        (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished
+                   shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment
+                        updatedDownloads:(nullable UpdatedDownloads)updatedDownloads
+                        transactionCache:(nonnull FIATransactionCache *)transactionCache;
 // Can throw exceptions if the transaction type is purchasing, should always used in a @try block.
 - (void)finishTransaction:(nonnull SKPaymentTransaction *)transaction;
 - (void)restoreTransactions:(nullable NSString *)applicationName;
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.m
index 2166795..59fdced 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.m
+++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.m
@@ -4,18 +4,49 @@
 
 #import "FIAPaymentQueueHandler.h"
 #import "FIAPPaymentQueueDelegate.h"
+#import "FIATransactionCache.h"
 
 @interface FIAPaymentQueueHandler ()
 
+/// The SKPaymentQueue instance connected to the App Store and responsible for processing
+/// transactions.
 @property(strong, nonatomic) SKPaymentQueue *queue;
+
+/// Callback method that is called each time the App Store indicates transactions are updated.
 @property(nullable, copy, nonatomic) TransactionsUpdated transactionsUpdated;
+
+/// Callback method that is called each time the App Store indicates transactions are removed.
 @property(nullable, copy, nonatomic) TransactionsRemoved transactionsRemoved;
+
+/// Callback method that is called each time the App Store indicates transactions failed to restore.
 @property(nullable, copy, nonatomic) RestoreTransactionFailed restoreTransactionFailed;
+
+/// Callback method that is called each time the App Store indicates restoring of transactions has
+/// finished.
 @property(nullable, copy, nonatomic)
     RestoreCompletedTransactionsFinished paymentQueueRestoreCompletedTransactionsFinished;
+
+/// Callback method that is called each time an in-app purchase has been initiated from the App
+/// Store.
 @property(nullable, copy, nonatomic) ShouldAddStorePayment shouldAddStorePayment;
+
+/// Callback method that is called each time the App Store indicates downloads are updated.
 @property(nullable, copy, nonatomic) UpdatedDownloads updatedDownloads;
 
+/// The transaction cache responsible for caching transactions.
+///
+/// Keeps track of transactions that arrive when the Flutter client is not
+/// actively observing for transactions.
+@property(strong, nonatomic, nonnull) FIATransactionCache *transactionCache;
+
+/// Indicates if the Flutter client is observing transactions.
+///
+/// When the client is not observing, transactions are cached and send to the
+/// client as soon as it starts observing. The Flutter client can start
+/// observing by sending a startObservingPaymentQueue message and stop by
+/// sending a stopObservingPaymentQueue message.
+@property(atomic, assign, readwrite, getter=isObservingTransactions) BOOL observingTransactions;
+
 @end
 
 @implementation FIAPaymentQueueHandler
@@ -28,6 +59,25 @@
         (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished
                    shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment
                         updatedDownloads:(nullable UpdatedDownloads)updatedDownloads {
+  return [[FIAPaymentQueueHandler alloc] initWithQueue:queue
+                                   transactionsUpdated:transactionsUpdated
+                                    transactionRemoved:transactionsRemoved
+                              restoreTransactionFailed:restoreTransactionFailed
+                  restoreCompletedTransactionsFinished:restoreCompletedTransactionsFinished
+                                 shouldAddStorePayment:shouldAddStorePayment
+                                      updatedDownloads:updatedDownloads
+                                      transactionCache:[[FIATransactionCache alloc] init]];
+}
+
+- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue
+                     transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated
+                      transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved
+                restoreTransactionFailed:(nullable RestoreTransactionFailed)restoreTransactionFailed
+    restoreCompletedTransactionsFinished:
+        (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished
+                   shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment
+                        updatedDownloads:(nullable UpdatedDownloads)updatedDownloads
+                        transactionCache:(nonnull FIATransactionCache *)transactionCache {
   self = [super init];
   if (self) {
     _queue = queue;
@@ -37,7 +87,9 @@
     _paymentQueueRestoreCompletedTransactionsFinished = restoreCompletedTransactionsFinished;
     _shouldAddStorePayment = shouldAddStorePayment;
     _updatedDownloads = updatedDownloads;
+    _transactionCache = transactionCache;
 
+    [_queue addTransactionObserver:self];
     if (@available(iOS 13.0, macOS 10.15, *)) {
       queue.delegate = self.delegate;
     }
@@ -46,11 +98,43 @@
 }
 
 - (void)startObservingPaymentQueue {
-  [_queue addTransactionObserver:self];
+  self.observingTransactions = YES;
+
+  [self processCachedTransactions];
 }
 
 - (void)stopObservingPaymentQueue {
-  [_queue removeTransactionObserver:self];
+  // When the client stops observing transaction, the transaction observer is
+  // not removed from the SKPaymentQueue. The FIAPaymentQueueHandler will cache
+  // trasnactions in memory when the client is not observing, allowing the app
+  // to process these transactions if it starts observing again during the same
+  // lifetime of the app.
+  //
+  // If the app is killed, cached transactions will be removed from memory;
+  // however, the App Store will re-deliver the transactions as soon as the app
+  // is started again, since the cached transactions have not been acknowledged
+  // by the client (by sending the `finishTransaction` message).
+  self.observingTransactions = NO;
+}
+
+- (void)processCachedTransactions {
+  NSArray *cachedObjects =
+      [self.transactionCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions];
+  if (cachedObjects.count != 0) {
+    self.transactionsUpdated(cachedObjects);
+  }
+
+  cachedObjects = [self.transactionCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads];
+  if (cachedObjects.count != 0) {
+    self.updatedDownloads(cachedObjects);
+  }
+
+  cachedObjects = [self.transactionCache getObjectsForKey:TransactionCacheKeyRemovedTransactions];
+  if (cachedObjects.count != 0) {
+    self.transactionsRemoved(cachedObjects);
+  }
+
+  [self.transactionCache clear];
 }
 
 - (BOOL)addPayment:(SKPayment *)payment {
@@ -93,6 +177,11 @@
 // state of transactions and finish as appropriate.
 - (void)paymentQueue:(SKPaymentQueue *)queue
     updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
+  if (!self.observingTransactions) {
+    [_transactionCache addObjects:transactions forKey:TransactionCacheKeyUpdatedTransactions];
+    return;
+  }
+
   // notify dart through callbacks.
   self.transactionsUpdated(transactions);
 }
@@ -100,6 +189,10 @@
 // Sent when transactions are removed from the queue (via finishTransaction:).
 - (void)paymentQueue:(SKPaymentQueue *)queue
     removedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
+  if (!self.observingTransactions) {
+    [_transactionCache addObjects:transactions forKey:TransactionCacheKeyRemovedTransactions];
+    return;
+  }
   self.transactionsRemoved(transactions);
 }
 
@@ -118,6 +211,10 @@
 
 // Sent when the download state has changed.
 - (void)paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray<SKDownload *> *)downloads {
+  if (!self.observingTransactions) {
+    [_transactionCache addObjects:downloads forKey:TransactionCacheKeyUpdatedDownloads];
+    return;
+  }
   self.updatedDownloads(downloads);
 }
 
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.h
new file mode 100644
index 0000000..dea3c2d
--- /dev/null
+++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.h
@@ -0,0 +1,31 @@
+// 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.
+
+NS_ASSUME_NONNULL_BEGIN
+
+typedef NS_ENUM(NSUInteger, TransactionCacheKey) {
+  TransactionCacheKeyUpdatedDownloads,
+  TransactionCacheKeyUpdatedTransactions,
+  TransactionCacheKeyRemovedTransactions
+};
+
+@interface FIATransactionCache : NSObject
+
+/// Adds objects to the transaction cache.
+///
+/// If the cache already contains an array of objects on the specified key, the supplied
+/// array will be appended to the existing array.
+- (void)addObjects:(NSArray *)objects forKey:(TransactionCacheKey)key;
+
+/// Gets the array of objects stored at the given key.
+///
+/// If there are no objects associated with the given key nil is returned.
+- (NSArray *)getObjectsForKey:(TransactionCacheKey)key;
+
+/// Removes all objects from the transaction cache.
+- (void)clear;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.m
new file mode 100644
index 0000000..f80b9c4
--- /dev/null
+++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.m
@@ -0,0 +1,40 @@
+// 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 "FIATransactionCache.h"
+
+@interface FIATransactionCache ()
+
+/// A NSMutableDictionary storing the objects that are cached.
+@property(nonatomic, strong, nonnull) NSMutableDictionary *cache;
+
+@end
+
+@implementation FIATransactionCache
+
+- (instancetype)init {
+  self = [super init];
+  if (self) {
+    self.cache = [[NSMutableDictionary alloc] init];
+  }
+
+  return self;
+}
+
+- (void)addObjects:(NSArray *)objects forKey:(TransactionCacheKey)key {
+  NSArray *cachedObjects = self.cache[@(key)];
+
+  self.cache[@(key)] =
+      cachedObjects ? [cachedObjects arrayByAddingObjectsFromArray:objects] : objects;
+}
+
+- (NSArray *)getObjectsForKey:(TransactionCacheKey)key {
+  return self.cache[@(key)];
+}
+
+- (void)clear {
+  [self.cache removeAllObjects];
+}
+
+@end
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m
index 661f57f..a580a46 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m
+++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m
@@ -79,7 +79,8 @@
       }
       updatedDownloads:^void(NSArray<SKDownload *> *_Nonnull downloads) {
         [weakSelf updatedDownloads:downloads];
-      }];
+      }
+      transactionCache:[[FIATransactionCache alloc] init]];
 
   _transactionObserverCallbackChannel =
       [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase"
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 7e70168..51b5ce7 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/main/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.3.0+2
+version: 0.3.0+3
 
 environment:
   sdk: ">=2.14.0 <3.0.0"