Browse Source

Merge pull request #610 from bizz84/develop

Some Bug Fixes & Improvements
Samuel Spencer 4 years ago
parent
commit
8fa93ef4d5

+ 2 - 0
CHANGELOG.md

@@ -2,6 +2,8 @@
 
 All notable changes to this project will be documented in this file.
 
+* Make `ProductsInfoController`'s `retrieveProductsInfo` thread safe ([#405]https://github.com/bizz84/SwiftyStoreKit/pull/495), related issues: [#344](https://github.com/bizz84/SwiftyStoreKit/issues/344) and [#468](https://github.com/bizz84/SwiftyStoreKit/issues/468)
+
 ## [0.15.0](https://github.com/bizz84/SwiftyStoreKit/releases/tag/0.15.0) Update project to Swift 5, Xcode 10.2
 
 * Update project to Swift 5 ([#457](https://github.com/bizz84/SwiftyStoreKit/pull/457)), related issue: [#456](https://github.com/bizz84/SwiftyStoreKit/issues/456)

+ 2 - 2
Package.swift

@@ -8,7 +8,7 @@ let package = Package(
         // Products define the executables and libraries produced by a package, and make them visible to other packages.
         .library(
             name: "SwiftyStoreKit",
-            targets: ["SwiftyStoreKit"]),
+            targets: ["SwiftyStoreKit"])
     ],
     dependencies: [
         // Dependencies declare other packages that this package depends on.
@@ -22,6 +22,6 @@ let package = Package(
             dependencies: []),
         .testTarget(
             name: "SwiftyStoreKitTests",
-            dependencies: ["SwiftyStoreKit"]),
+            dependencies: ["SwiftyStoreKit"])
     ]
 )

+ 2 - 0
README.md

@@ -798,6 +798,8 @@ It would be great to showcase apps using SwiftyStoreKit here. Pull requests welc
 * [Talk Dim Sum](https://itunes.apple.com/us/app/talk-dim-sum/id953929066) - Your dim sum companion
 * [Sluggard](https://itunes.apple.com/app/id1160131071) - Perform simple exercises to reduce the risks of sedentary lifestyle
 * [Debts iOS](https://debts.ivanvorobei.by/ios) & [Debts macOS](https://debts.ivanvorobei.by/macos) - Track amounts owed
+* [Botcher](https://itunes.apple.com/us/app/id1522337788) - Good for finding something to do
+* [Hashr](https://apps.apple.com/app/id1166499829) - Generate unique password hashes based on website and master password
 
 A full list of apps is published [on AppSight](https://www.appsight.io/sdk/574154).
 

+ 15 - 3
Sources/SwiftyStoreKit/InAppProductQueryRequest.swift

@@ -31,13 +31,20 @@ public protocol InAppRequest: class {
     func cancel()
 }
 
-protocol InAppProductRequest: InAppRequest { }
+protocol InAppProductRequest: InAppRequest {
+    var hasCompleted: Bool { get }
+    var cachedResults: RetrieveResults? { get }
+}
 
 class InAppProductQueryRequest: NSObject, InAppProductRequest, SKProductsRequestDelegate {
 
     private let callback: InAppProductRequestCallback
     private let request: SKProductsRequest
 
+    private(set) var cachedResults: RetrieveResults?
+
+    var hasCompleted: Bool { cachedResults != nil }
+
     deinit {
         request.delegate = nil
     }
@@ -52,6 +59,7 @@ class InAppProductQueryRequest: NSObject, InAppProductRequest, SKProductsRequest
     func start() {
         request.start()
     }
+
     func cancel() {
         request.cancel()
     }
@@ -61,8 +69,12 @@ class InAppProductQueryRequest: NSObject, InAppProductRequest, SKProductsRequest
 
         let retrievedProducts = Set<SKProduct>(response.products)
         let invalidProductIDs = Set<String>(response.invalidProductIdentifiers)
-        performCallback(RetrieveResults(retrievedProducts: retrievedProducts,
-            invalidProductIDs: invalidProductIDs, error: nil))
+        let results = RetrieveResults(
+            retrievedProducts: retrievedProducts,
+            invalidProductIDs: invalidProductIDs, error: nil
+        )
+        self.cachedResults = results
+        performCallback(results)
     }
 
     func requestDidFinish(_ request: SKRequest) {

+ 1 - 1
Sources/SwiftyStoreKit/OS.swift

@@ -46,7 +46,7 @@ public struct SKError: Error {
         self._nsError = _nsError
     }
     
-    var code: Code {
+    public var code: Code {
         return Code(rawValue: _nsError.code) ?? .unknown
     }
     

+ 26 - 2
Sources/SwiftyStoreKit/PaymentQueueController.swift

@@ -88,6 +88,14 @@ extension SKPaymentTransactionState: CustomDebugStringConvertible {
     }
 }
 
+struct EntitlementRevocation {
+    let callback: ([String]) -> Void
+
+    init(callback: @escaping ([String]) -> Void) {
+        self.callback = callback
+    }
+}
+
 class PaymentQueueController: NSObject, SKPaymentTransactionObserver {
     
     private let paymentsController: PaymentsController
@@ -97,7 +105,9 @@ class PaymentQueueController: NSObject, SKPaymentTransactionObserver {
     private let completeTransactionsController: CompleteTransactionsController
     
     unowned let paymentQueue: PaymentQueue
-    
+
+    private var entitlementRevocation: EntitlementRevocation?
+
     deinit {
         paymentQueue.remove(self)
     }
@@ -145,6 +155,15 @@ class PaymentQueueController: NSObject, SKPaymentTransactionObserver {
         paymentsController.append(payment)
     }
     
+    func onEntitlementRevocation(_ revocation: EntitlementRevocation) {
+        guard entitlementRevocation == nil else {
+            print("SwiftyStoreKit.onEntitlementRevocation() should only be called once when the app launches. Ignoring this call")
+            return
+        }
+
+        self.entitlementRevocation = revocation
+    }
+
     func restorePurchases(_ restorePurchases: RestorePurchases) {
         assertCompleteTransactionsWasCalled()
         
@@ -206,7 +225,7 @@ class PaymentQueueController: NSObject, SKPaymentTransactionObserver {
          * Failed transactions only ever belong to queued payment requests.
          * restoreCompletedTransactionsFailedWithError is always called when a restore purchases request fails.
          * paymentQueueRestoreCompletedTransactionsFinished is always called following 0 or more update transactions when a restore purchases request succeeds.
-         * A complete transactions handler is require to catch any transactions that are updated when the app is not running.
+         * A complete transactions handler is required to catch any transactions that are updated when the app is not running.
          * Registering a complete transactions handler when the app launches ensures that any pending transactions can be cleared.
          * If a complete transactions handler is missing, pending transactions can be mis-attributed to any new incoming payments or restore purchases.
          *
@@ -233,6 +252,11 @@ class PaymentQueueController: NSObject, SKPaymentTransactionObserver {
         }
     }
     
+    func paymentQueue(_ queue: SKPaymentQueue, didRevokeEntitlementsForProductIdentifiers productIdentifiers: [String]) {
+
+        self.entitlementRevocation?.callback(productIdentifiers)
+    }
+    
     func paymentQueue(_ queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
         
     }

+ 12 - 0
Sources/SwiftyStoreKit/ProductsInfoController.swift

@@ -69,9 +69,21 @@ class ProductsInfoController: NSObject {
             }
             inflightRequests[productIds] = InAppProductQuery(request: request, completionHandlers: [completion])
             request.start()
+
             return request
+
         } else {
+            
             inflightRequests[productIds]!.completionHandlers.append(completion)
+
+            let query = inflightRequests[productIds]!
+
+            if query.request.hasCompleted {
+                query.completionHandlers.forEach {
+                    $0(query.request.cachedResults!)
+                }
+            }
+
             return inflightRequests[productIds]!.request
         }
     }

+ 0 - 1
Sources/SwiftyStoreKit/SKProductDiscount+LocalizedPrice.swift

@@ -60,4 +60,3 @@ public extension SKProductDiscount {
     }
     
 }
-

+ 1 - 1
Sources/SwiftyStoreKit/SwiftyStoreKit+Types.swift

@@ -175,7 +175,7 @@ public enum VerifySubscriptionResult {
     case notPurchased
 }
 
-public enum SubscriptionType {
+public enum SubscriptionType: Hashable {
     case autoRenewable
     case nonRenewing(validDuration: TimeInterval)
 }

+ 13 - 0
Sources/SwiftyStoreKit/SwiftyStoreKit.swift

@@ -84,6 +84,11 @@ public class SwiftyStoreKit {
         
         paymentQueueController.completeTransactions(CompleteTransactions(atomically: atomically, callback: completion))
     }
+
+    fileprivate func onEntitlementRevocation(completion: @escaping ([String]) -> Void) {
+
+        paymentQueueController.onEntitlementRevocation(EntitlementRevocation(callback: completion))
+    }
     
     fileprivate func finishTransaction(_ transaction: PaymentTransaction) {
         
@@ -187,6 +192,14 @@ extension SwiftyStoreKit {
         
         sharedInstance.completeTransactions(atomically: atomically, completion: completion)
     }
+
+    /// Entitlement revocation notification
+    ///   - Parameter completion: handler for result (list of product identifiers revoked)
+    @available(iOS 14, tvOS 14, OSX 11, watchOS 7, macCatalyst 14, *)
+    public class func onEntitlementRevocation(completion: @escaping ([String]) -> Void) {
+
+        sharedInstance.onEntitlementRevocation(completion: completion)
+    }
     
     /// Finish a transaction
     /// 

+ 7 - 17
SwiftyStoreKit.xcodeproj/project.pbxproj

@@ -71,9 +71,6 @@
 		2F2B8B7524A64CD700CEF088 /* SwiftyStoreKit-watchOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 2F2B8B6C24A64CD700CEF088 /* SwiftyStoreKit-watchOS.h */; };
 		2F2B8B7624A64CD700CEF088 /* SwiftyStoreKit-watchOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 2F2B8B6C24A64CD700CEF088 /* SwiftyStoreKit-watchOS.h */; };
 		2F2B8B7724A64CD700CEF088 /* SwiftyStoreKit-watchOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 2F2B8B6C24A64CD700CEF088 /* SwiftyStoreKit-watchOS.h */; };
-		2F2B8B7924A64CD700CEF088 /* Info-macOS.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2F2B8B6D24A64CD700CEF088 /* Info-macOS.plist */; };
-		2F2B8B7F24A64CD700CEF088 /* Info-watchOS.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2F2B8B6E24A64CD700CEF088 /* Info-watchOS.plist */; };
-		2F2B8B8024A64CD700CEF088 /* Info-iOS.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2F2B8B6F24A64CD700CEF088 /* Info-iOS.plist */; };
 		2F2B8B8424A64CD700CEF088 /* SwiftyStoreKit-tvOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 2F2B8B7024A64CD700CEF088 /* SwiftyStoreKit-tvOS.h */; };
 		2F2B8B8524A64CD700CEF088 /* SwiftyStoreKit-tvOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 2F2B8B7024A64CD700CEF088 /* SwiftyStoreKit-tvOS.h */; };
 		2F2B8B8624A64CD700CEF088 /* SwiftyStoreKit-tvOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 2F2B8B7024A64CD700CEF088 /* SwiftyStoreKit-tvOS.h */; };
@@ -86,7 +83,6 @@
 		2F2B8B8D24A64CD700CEF088 /* SwiftyStoreKit-macOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 2F2B8B7224A64CD700CEF088 /* SwiftyStoreKit-macOS.h */; };
 		2F2B8B8E24A64CD700CEF088 /* SwiftyStoreKit-macOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 2F2B8B7224A64CD700CEF088 /* SwiftyStoreKit-macOS.h */; };
 		2F2B8B8F24A64CD700CEF088 /* SwiftyStoreKit-macOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 2F2B8B7224A64CD700CEF088 /* SwiftyStoreKit-macOS.h */; };
-		2F2B8B9224A64CD700CEF088 /* Info-tvOS.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2F2B8B7324A64CD700CEF088 /* Info-tvOS.plist */; };
 		2F2B8BA024A64DE600CEF088 /* PaymentQueueControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2B8B9424A64DE600CEF088 /* PaymentQueueControllerTests.swift */; };
 		2F2B8BA124A64DE600CEF088 /* PaymentTransactionObserverFake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2B8B9524A64DE600CEF088 /* PaymentTransactionObserverFake.swift */; };
 		2F2B8BA224A64DE600CEF088 /* ProductsInfoControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2B8B9624A64DE600CEF088 /* ProductsInfoControllerTests.swift */; };
@@ -97,7 +93,6 @@
 		2F2B8BA724A64DE600CEF088 /* InAppReceiptVerificatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2B8B9B24A64DE600CEF088 /* InAppReceiptVerificatorTests.swift */; };
 		2F2B8BA824A64DE600CEF088 /* InAppReceiptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2B8B9C24A64DE600CEF088 /* InAppReceiptTests.swift */; };
 		2F2B8BA924A64DE600CEF088 /* PaymentsControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2B8B9D24A64DE600CEF088 /* PaymentsControllerTests.swift */; };
-		2F2B8BAA24A64DE600CEF088 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2F2B8B9E24A64DE600CEF088 /* Info.plist */; };
 		2F2B8BAB24A64DE600CEF088 /* PaymentQueueSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2B8B9F24A64DE600CEF088 /* PaymentQueueSpy.swift */; };
 		654287F61E79F5A000F61800 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 654287F41E79F5A000F61800 /* Main.storyboard */; };
 		654287F81E79F5A000F61800 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 654287F71E79F5A000F61800 /* Assets.xcassets */; };
@@ -200,7 +195,7 @@
 /* Begin PBXFileReference section */
 		2F2B8B2124A64CC000CEF088 /* SKProductDiscount+LocalizedPrice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "SKProductDiscount+LocalizedPrice.swift"; path = "Sources/SwiftyStoreKit/SKProductDiscount+LocalizedPrice.swift"; sourceTree = SOURCE_ROOT; };
 		2F2B8B2224A64CC000CEF088 /* AppleReceiptValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppleReceiptValidator.swift; path = Sources/SwiftyStoreKit/AppleReceiptValidator.swift; sourceTree = SOURCE_ROOT; };
-		2F2B8B2324A64CC000CEF088 /* InAppProductQueryRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InAppProductQueryRequest.swift; path = Sources/SwiftyStoreKit/InAppProductQueryRequest.swift; sourceTree = SOURCE_ROOT; };
+		2F2B8B2324A64CC000CEF088 /* InAppProductQueryRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; name = InAppProductQueryRequest.swift; path = Sources/SwiftyStoreKit/InAppProductQueryRequest.swift; sourceTree = SOURCE_ROOT; };
 		2F2B8B2424A64CC000CEF088 /* InAppReceipt.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InAppReceipt.swift; path = Sources/SwiftyStoreKit/InAppReceipt.swift; sourceTree = SOURCE_ROOT; };
 		2F2B8B2524A64CC000CEF088 /* SwiftyStoreKit+Types.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "SwiftyStoreKit+Types.swift"; path = "Sources/SwiftyStoreKit/SwiftyStoreKit+Types.swift"; sourceTree = SOURCE_ROOT; };
 		2F2B8B2624A64CC000CEF088 /* CompleteTransactionsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CompleteTransactionsController.swift; path = Sources/SwiftyStoreKit/CompleteTransactionsController.swift; sourceTree = SOURCE_ROOT; };
@@ -208,7 +203,7 @@
 		2F2B8B2824A64CC000CEF088 /* InAppReceiptRefreshRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InAppReceiptRefreshRequest.swift; path = Sources/SwiftyStoreKit/InAppReceiptRefreshRequest.swift; sourceTree = SOURCE_ROOT; };
 		2F2B8B2924A64CC100CEF088 /* InAppReceiptVerificator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InAppReceiptVerificator.swift; path = Sources/SwiftyStoreKit/InAppReceiptVerificator.swift; sourceTree = SOURCE_ROOT; };
 		2F2B8B2A24A64CC100CEF088 /* SKProduct+LocalizedPrice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "SKProduct+LocalizedPrice.swift"; path = "Sources/SwiftyStoreKit/SKProduct+LocalizedPrice.swift"; sourceTree = SOURCE_ROOT; };
-		2F2B8B2B24A64CC100CEF088 /* ProductsInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProductsInfoController.swift; path = Sources/SwiftyStoreKit/ProductsInfoController.swift; sourceTree = SOURCE_ROOT; };
+		2F2B8B2B24A64CC100CEF088 /* ProductsInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; name = ProductsInfoController.swift; path = Sources/SwiftyStoreKit/ProductsInfoController.swift; sourceTree = SOURCE_ROOT; };
 		2F2B8B2C24A64CC100CEF088 /* OS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OS.swift; path = Sources/SwiftyStoreKit/OS.swift; sourceTree = SOURCE_ROOT; };
 		2F2B8B2D24A64CC100CEF088 /* PaymentsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PaymentsController.swift; path = Sources/SwiftyStoreKit/PaymentsController.swift; sourceTree = SOURCE_ROOT; };
 		2F2B8B2E24A64CC100CEF088 /* SwiftyStoreKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SwiftyStoreKit.swift; path = Sources/SwiftyStoreKit/SwiftyStoreKit.swift; sourceTree = SOURCE_ROOT; };
@@ -231,7 +226,7 @@
 		2F2B8B9B24A64DE600CEF088 /* InAppReceiptVerificatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InAppReceiptVerificatorTests.swift; path = Tests/SwiftyStoreKitTests/InAppReceiptVerificatorTests.swift; sourceTree = SOURCE_ROOT; };
 		2F2B8B9C24A64DE600CEF088 /* InAppReceiptTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InAppReceiptTests.swift; path = Tests/SwiftyStoreKitTests/InAppReceiptTests.swift; sourceTree = SOURCE_ROOT; };
 		2F2B8B9D24A64DE600CEF088 /* PaymentsControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PaymentsControllerTests.swift; path = Tests/SwiftyStoreKitTests/PaymentsControllerTests.swift; sourceTree = SOURCE_ROOT; };
-		2F2B8B9E24A64DE600CEF088 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = Info.plist; path = Tests/SwiftyStoreKitTests/Info.plist; sourceTree = SOURCE_ROOT; };
+		2F2B8B9E24A64DE600CEF088 /* Info-Tests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "Info-Tests.plist"; path = "Tests/SwiftyStoreKitTests/Info-Tests.plist"; sourceTree = SOURCE_ROOT; };
 		2F2B8B9F24A64DE600CEF088 /* PaymentQueueSpy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PaymentQueueSpy.swift; path = Tests/SwiftyStoreKitTests/PaymentQueueSpy.swift; sourceTree = SOURCE_ROOT; };
 		54C0D52C1CF7404500F90BCE /* SwiftyStoreKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftyStoreKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		6502F5FE1B985833004E342D /* SwiftyStoreKit_iOSDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftyStoreKit_iOSDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -388,7 +383,7 @@
 				2F2B8B9A24A64DE600CEF088 /* CompleteTransactionsControllerTests.swift */,
 				2F2B8B9C24A64DE600CEF088 /* InAppReceiptTests.swift */,
 				2F2B8B9B24A64DE600CEF088 /* InAppReceiptVerificatorTests.swift */,
-				2F2B8B9E24A64DE600CEF088 /* Info.plist */,
+				2F2B8B9E24A64DE600CEF088 /* Info-Tests.plist */,
 				2F2B8B9424A64DE600CEF088 /* PaymentQueueControllerTests.swift */,
 				2F2B8B9F24A64DE600CEF088 /* PaymentQueueSpy.swift */,
 				2F2B8B9D24A64DE600CEF088 /* PaymentsControllerTests.swift */,
@@ -650,7 +645,7 @@
 			isa = PBXProject;
 			attributes = {
 				LastSwiftUpdateCheck = 0820;
-				LastUpgradeCheck = 1020;
+				LastUpgradeCheck = 1160;
 				ORGANIZATIONNAME = musevisions;
 				TargetAttributes = {
 					54C0D52B1CF7404500F90BCE = {
@@ -720,7 +715,6 @@
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				2F2B8B9224A64CD700CEF088 /* Info-tvOS.plist in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -738,7 +732,6 @@
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				2F2B8B8024A64CD700CEF088 /* Info-iOS.plist in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -755,7 +748,6 @@
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				2F2B8BAA24A64DE600CEF088 /* Info.plist in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -763,7 +755,6 @@
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				2F2B8B7F24A64CD700CEF088 /* Info-watchOS.plist in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -771,7 +762,6 @@
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				2F2B8B7924A64CD700CEF088 /* Info-macOS.plist in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -1334,7 +1324,7 @@
 				CLANG_ANALYZER_NONNULL = YES;
 				CLANG_ENABLE_MODULES = YES;
 				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
-				INFOPLIST_FILE = SwiftyStoreKitTests/Info.plist;
+				INFOPLIST_FILE = "Tests/SwiftyStoreKitTests/Info-Tests.plist";
 				IPHONEOS_DEPLOYMENT_TARGET = 10.2;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
 				PRODUCT_BUNDLE_IDENTIFIER = com.musevisions.iOS.SwiftyStoreKitTests;
@@ -1354,7 +1344,7 @@
 				CLANG_ANALYZER_NONNULL = YES;
 				CLANG_ENABLE_MODULES = YES;
 				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
-				INFOPLIST_FILE = SwiftyStoreKitTests/Info.plist;
+				INFOPLIST_FILE = "Tests/SwiftyStoreKitTests/Info-Tests.plist";
 				IPHONEOS_DEPLOYMENT_TARGET = 10.2;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
 				PRODUCT_BUNDLE_IDENTIFIER = com.musevisions.iOS.SwiftyStoreKitTests;

+ 10 - 14
SwiftyStoreKit.xcodeproj/xcshareddata/xcschemes/SwiftyStoreKit-iOS-Demo.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1020"
+   LastUpgradeVersion = "1160"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
@@ -27,6 +27,15 @@
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
       shouldUseLaunchSchemeArgsEnv = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "6502F5FD1B985833004E342D"
+            BuildableName = "SwiftyStoreKit_iOSDemo.app"
+            BlueprintName = "SwiftyStoreKit_iOSDemo"
+            ReferencedContainer = "container:SwiftyStoreKit.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
       <Testables>
          <TestableReference
             skipped = "NO">
@@ -39,17 +48,6 @@
             </BuildableReference>
          </TestableReference>
       </Testables>
-      <MacroExpansion>
-         <BuildableReference
-            BuildableIdentifier = "primary"
-            BlueprintIdentifier = "6502F5FD1B985833004E342D"
-            BuildableName = "SwiftyStoreKit_iOSDemo.app"
-            BlueprintName = "SwiftyStoreKit_iOSDemo"
-            ReferencedContainer = "container:SwiftyStoreKit.xcodeproj">
-         </BuildableReference>
-      </MacroExpansion>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </TestAction>
    <LaunchAction
       buildConfiguration = "Debug"
@@ -71,8 +69,6 @@
             ReferencedContainer = "container:SwiftyStoreKit.xcodeproj">
          </BuildableReference>
       </BuildableProductRunnable>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </LaunchAction>
    <ProfileAction
       buildConfiguration = "Release"

+ 1 - 5
SwiftyStoreKit.xcodeproj/xcshareddata/xcschemes/SwiftyStoreKit-iOS.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1020"
+   LastUpgradeVersion = "1160"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
@@ -29,8 +29,6 @@
       shouldUseLaunchSchemeArgsEnv = "YES">
       <Testables>
       </Testables>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </TestAction>
    <LaunchAction
       buildConfiguration = "Debug"
@@ -51,8 +49,6 @@
             ReferencedContainer = "container:SwiftyStoreKit.xcodeproj">
          </BuildableReference>
       </MacroExpansion>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </LaunchAction>
    <ProfileAction
       buildConfiguration = "Release"

+ 3 - 7
SwiftyStoreKit.xcodeproj/xcshareddata/xcschemes/SwiftyStoreKit-macOS-Demo.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1020"
+   LastUpgradeVersion = "1160"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
@@ -27,8 +27,6 @@
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
       shouldUseLaunchSchemeArgsEnv = "YES">
-      <Testables>
-      </Testables>
       <MacroExpansion>
          <BuildableReference
             BuildableIdentifier = "primary"
@@ -38,8 +36,8 @@
             ReferencedContainer = "container:SwiftyStoreKit.xcodeproj">
          </BuildableReference>
       </MacroExpansion>
-      <AdditionalOptions>
-      </AdditionalOptions>
+      <Testables>
+      </Testables>
    </TestAction>
    <LaunchAction
       buildConfiguration = "Debug"
@@ -61,8 +59,6 @@
             ReferencedContainer = "container:SwiftyStoreKit.xcodeproj">
          </BuildableReference>
       </BuildableProductRunnable>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </LaunchAction>
    <ProfileAction
       buildConfiguration = "Release"

+ 1 - 5
SwiftyStoreKit.xcodeproj/xcshareddata/xcschemes/SwiftyStoreKit-macOS.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1020"
+   LastUpgradeVersion = "1160"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
@@ -29,8 +29,6 @@
       shouldUseLaunchSchemeArgsEnv = "YES">
       <Testables>
       </Testables>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </TestAction>
    <LaunchAction
       buildConfiguration = "Debug"
@@ -51,8 +49,6 @@
             ReferencedContainer = "container:SwiftyStoreKit.xcodeproj">
          </BuildableReference>
       </MacroExpansion>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </LaunchAction>
    <ProfileAction
       buildConfiguration = "Release"

+ 3 - 7
SwiftyStoreKit.xcodeproj/xcshareddata/xcschemes/SwiftyStoreKit-tvOS-Demo.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1020"
+   LastUpgradeVersion = "1160"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
@@ -27,8 +27,6 @@
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
       shouldUseLaunchSchemeArgsEnv = "YES">
-      <Testables>
-      </Testables>
       <MacroExpansion>
          <BuildableReference
             BuildableIdentifier = "primary"
@@ -38,8 +36,8 @@
             ReferencedContainer = "container:SwiftyStoreKit.xcodeproj">
          </BuildableReference>
       </MacroExpansion>
-      <AdditionalOptions>
-      </AdditionalOptions>
+      <Testables>
+      </Testables>
    </TestAction>
    <LaunchAction
       buildConfiguration = "Debug"
@@ -61,8 +59,6 @@
             ReferencedContainer = "container:SwiftyStoreKit.xcodeproj">
          </BuildableReference>
       </BuildableProductRunnable>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </LaunchAction>
    <ProfileAction
       buildConfiguration = "Release"

+ 1 - 5
SwiftyStoreKit.xcodeproj/xcshareddata/xcschemes/SwiftyStoreKit-tvOS.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1020"
+   LastUpgradeVersion = "1160"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
@@ -29,8 +29,6 @@
       shouldUseLaunchSchemeArgsEnv = "YES">
       <Testables>
       </Testables>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </TestAction>
    <LaunchAction
       buildConfiguration = "Debug"
@@ -51,8 +49,6 @@
             ReferencedContainer = "container:SwiftyStoreKit.xcodeproj">
          </BuildableReference>
       </MacroExpansion>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </LaunchAction>
    <ProfileAction
       buildConfiguration = "Release"

+ 1 - 5
SwiftyStoreKit.xcodeproj/xcshareddata/xcschemes/SwiftyStoreKitTests.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1020"
+   LastUpgradeVersion = "1160"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
@@ -23,8 +23,6 @@
             </BuildableReference>
          </TestableReference>
       </Testables>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </TestAction>
    <LaunchAction
       buildConfiguration = "Debug"
@@ -36,8 +34,6 @@
       debugDocumentVersioning = "YES"
       debugServiceExtension = "internal"
       allowLocationSimulation = "YES">
-      <AdditionalOptions>
-      </AdditionalOptions>
    </LaunchAction>
    <ProfileAction
       buildConfiguration = "Release"

+ 1 - 1
SwiftyStoreKit.xcodeproj/xcshareddata/xcschemes/SwiftyStoreKit_watchOS.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1150"
+   LastUpgradeVersion = "1160"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"

+ 3 - 20
Tests/SwiftyStoreKitTests/InAppReceiptTests.swift

@@ -107,12 +107,11 @@ extension VerifyPurchaseResult: Equatable {
     }
 }
 
-// swiftlint:disable file_length
+// swiftlint: disable file_length
 class InAppReceiptTests: XCTestCase {
 
     // MARK: Verify Purchase
     func testVerifyPurchase_when_noPurchases_then_resultIsNotPurchased() {
-
         let receiptRequestDate = makeDateAtMidnight(year: 2017, month: 5, day: 14)
         let productId = "product1"
         let receipt = makeReceipt(items: [], requestDate: receiptRequestDate)
@@ -121,8 +120,8 @@ class InAppReceiptTests: XCTestCase {
 
         XCTAssertEqual(verifyPurchaseResult, .notPurchased)
     }
+    
     func testVerifyPurchase_when_onePurchase_then_resultIsPurchased() {
-
         let receiptRequestDate = makeDateAtMidnight(year: 2017, month: 5, day: 14)
         let productId = "product1"
         let item = ReceiptItem(productId: productId, purchaseDate: receiptRequestDate, subscriptionExpirationDate: nil, cancellationDate: nil, isTrialPeriod: false)
@@ -132,8 +131,8 @@ class InAppReceiptTests: XCTestCase {
 
         XCTAssertEqual(verifyPurchaseResult, .purchased(item: item))
     }
+    
     func testVerifyPurchase_when_oneCancelledPurchase_then_resultIsNotPurchased() {
-
         let receiptRequestDate = makeDateAtMidnight(year: 2017, month: 5, day: 14)
         let productId = "product1"
         let item = ReceiptItem(productId: productId, purchaseDate: receiptRequestDate, subscriptionExpirationDate: nil, cancellationDate: receiptRequestDate, isTrialPeriod: false)
@@ -147,7 +146,6 @@ class InAppReceiptTests: XCTestCase {
     // MARK: Verify Subscription, single receipt item tests
     // auto-renewable, not purchased
     func testVerifyAutoRenewableSubscription_when_noSubscriptions_then_resultIsNotPurchased() {
-
         let receiptRequestDate = makeDateAtMidnight(year: 2017, month: 5, day: 14)
         let productId = "product1"
         let receipt = makeReceipt(items: [], requestDate: receiptRequestDate)
@@ -160,7 +158,6 @@ class InAppReceiptTests: XCTestCase {
 
     // auto-renewable, expired
     func testVerifyAutoRenewableSubscription_when_oneExpiredSubscription_then_resultIsExpired() {
-
         let receiptRequestDate = makeDateAtMidnight(year: 2017, month: 5, day: 15)
         let productId = "product1"
         let isTrialPeriod = false
@@ -177,7 +174,6 @@ class InAppReceiptTests: XCTestCase {
 
     // auto-renewable, purchased
     func testVerifyAutoRenewableSubscription_when_oneNonExpiredSubscription_then_resultIsPurchased() {
-
         let receiptRequestDate = makeDateAtMidnight(year: 2017, month: 5, day: 14)
         let productId = "product1"
         let isTrialPeriod = false
@@ -194,7 +190,6 @@ class InAppReceiptTests: XCTestCase {
 
     // auto-renewable, cancelled
     func testVerifyAutoRenewableSubscription_when_oneCancelledSubscription_then_resultIsNotPurchased() {
-
         let receiptRequestDate = makeDateAtMidnight(year: 2017, month: 5, day: 14)
         let productId = "product1"
         let isTrialPeriod = false
@@ -212,7 +207,6 @@ class InAppReceiptTests: XCTestCase {
 
     // non-renewing, non purchased
     func testVerifyNonRenewingSubscription_when_noSubscriptions_then_resultIsNotPurchased() {
-
         let receiptRequestDate = makeDateAtMidnight(year: 2017, month: 5, day: 14)
         let productId = "product1"
         let receipt = makeReceipt(items: [], requestDate: receiptRequestDate)
@@ -225,7 +219,6 @@ class InAppReceiptTests: XCTestCase {
 
     // non-renewing, expired
     func testVerifyNonRenewingSubscription_when_oneExpiredSubscription_then_resultIsExpired() {
-
         let receiptRequestDate = makeDateAtMidnight(year: 2017, month: 5, day: 15)
         let productId = "product1"
         let isTrialPeriod = false
@@ -244,7 +237,6 @@ class InAppReceiptTests: XCTestCase {
 
     // non-renewing, purchased
     func testVerifyNonRenewingSubscription_when_oneNonExpiredSubscription_then_resultIsPurchased() {
-
         let receiptRequestDate = makeDateAtMidnight(year: 2017, month: 5, day: 14)
         let productId = "product1"
         let isTrialPeriod = false
@@ -263,7 +255,6 @@ class InAppReceiptTests: XCTestCase {
 
     // non-renewing, cancelled
     func testVerifyNonRenewingSubscription_when_oneCancelledSubscription_then_resultIsNotPurchased() {
-
         let receiptRequestDate = makeDateAtMidnight(year: 2017, month: 5, day: 14)
         let productId = "product1"
         let isTrialPeriod = false
@@ -281,7 +272,6 @@ class InAppReceiptTests: XCTestCase {
 
     // MARK: Verify Subscription, multiple receipt item tests
     func testVerifyAutoRenewableSubscription_when_twoSubscriptions_sameProductId_mostRecentNonExpired_then_resultIsPurchased_itemsSorted() {
-
         let receiptRequestDate = makeDateAtMidnight(year: 2017, month: 5, day: 14)
 
         let productId = "product1"
@@ -312,7 +302,6 @@ class InAppReceiptTests: XCTestCase {
     }
 
     func testVerifyAutoRenewableSubscription_when_twoSubscriptions_sameProductId_bothExpired_then_resultIsExpired_itemsSorted() {
-        
         let receiptRequestDate = makeDateAtMidnight(year: 2017, month: 5, day: 14)
         
         let productId = "product1"
@@ -344,7 +333,6 @@ class InAppReceiptTests: XCTestCase {
     
     // MARK: Verify Subscriptions, multiple receipt item tests
     func testVerifyAutoRenewableSubscriptions_when_threeSubscriptions_twoMatchingProductIds_mostRecentNonExpired_then_resultIsPurchased_itemsSorted() {
-        
         let receiptRequestDate = makeDateAtMidnight(year: 2017, month: 5, day: 14)
         
         let productId1 = "product1"
@@ -386,7 +374,6 @@ class InAppReceiptTests: XCTestCase {
     
     // MARK: Get Distinct Purchase Identifiers, empty receipt item tests
     func testGetDistinctPurchaseIds_when_noReceipt_then_resultIsNil() {
-
         let receiptRequestDate = makeDateAtMidnight(year: 2017, month: 5, day: 14)
         let receipt = makeReceipt(items: [], requestDate: receiptRequestDate)
 
@@ -396,7 +383,6 @@ class InAppReceiptTests: XCTestCase {
     
     // MARK: Get Distinct Purchase Identifiers, multiple receipt item tests
     func testGetDistinctPurchaseIds_when_Receipt_then_resultIsNotNil() {
-
         let receiptRequestDateOne = makeDateAtMidnight(year: 2020, month: 2, day: 20)
         let purchaseDateOne = makeDateAtMidnight(year: 2020, month: 2, day: 1)
         let purchaseDateTwo = makeDateAtMidnight(year: 2020, month: 1, day: 1)
@@ -416,7 +402,6 @@ class InAppReceiptTests: XCTestCase {
     
     // MARK: Get Distinct Purchase Identifiers, multiple non unique product identifiers tests
     func testGetDistinctPurchaseIds_when_nonUniqueIdentifiers_then_resultIsUnique() {
-
         let receiptRequestDateOne = makeDateAtMidnight(year: 2020, month: 2, day: 20)
         let purchaseDateOne = makeDateAtMidnight(year: 2020, month: 2, day: 1)
         let purchaseDateTwo = makeDateAtMidnight(year: 2020, month: 2, day: 2)
@@ -442,7 +427,6 @@ class InAppReceiptTests: XCTestCase {
 
     // MARK: Helper methods
     func makeReceipt(items: [ReceiptItem], requestDate: Date) -> [String: AnyObject] {
-
         let receiptInfos = items.map { $0.receiptInfo }
 
         // Creating this with NSArray results in __NSSingleObjectArrayI which fails the cast to [String: AnyObject]
@@ -462,7 +446,6 @@ class InAppReceiptTests: XCTestCase {
     }
 
     func makeDateAtMidnight(year: Int, month: Int, day: Int) -> Date {
-
         var dateComponents = DateComponents()
         dateComponents.day = day
         dateComponents.month = month

+ 0 - 0
Tests/SwiftyStoreKitTests/Info.plist → Tests/SwiftyStoreKitTests/Info-Tests.plist


+ 55 - 0
Tests/SwiftyStoreKitTests/ProductsInfoControllerTests.swift

@@ -23,6 +23,7 @@
 // THE SOFTWARE.
 
 import XCTest
+import Foundation
 @testable import SwiftyStoreKit
 
 class TestInAppProductRequest: InAppProductRequest {
@@ -50,8 +51,15 @@ class TestInAppProductRequest: InAppProductRequest {
 class TestInAppProductRequestBuilder: InAppProductRequestBuilder {
     
     var requests: [ TestInAppProductRequest ] = []
+    var os_unfair_lock_s = os_unfair_lock()
     
     func request(productIds: Set<String>, callback: @escaping InAppProductRequestCallback) -> InAppProductRequest {
+        // add locks to make sure the test does not fail in preparation
+        os_unfair_lock_lock(&self.os_unfair_lock_s)
+        defer {
+          os_unfair_lock_unlock(&self.os_unfair_lock_s)
+        }
+      
         let request = TestInAppProductRequest(productIds: productIds, callback: callback)
         requests.append(request)
         return request
@@ -68,6 +76,17 @@ class TestInAppProductRequestBuilder: InAppProductRequestBuilder {
 class ProductsInfoControllerTests: XCTestCase {
     
     let sampleProductIdentifiers: Set<String> = ["com.iap.purchase1"]
+    // Set of in app purchases to ask in different threads
+    let testProducts: Set<String> = ["com.iap.purchase01",
+                                     "com.iap.purchase02",
+                                     "com.iap.purchase03",
+                                     "com.iap.purchase04",
+                                     "com.iap.purchase05",
+                                     "com.iap.purchase06",
+                                     "com.iap.purchase07",
+                                     "com.iap.purchase08",
+                                     "com.iap.purchase09",
+                                     "com.iap.purchase10"]
 
     func testRetrieveProductsInfo_when_calledOnce_then_completionCalledOnce() {
         
@@ -117,4 +136,40 @@ class ProductsInfoControllerTests: XCTestCase {
         requestBuilder.fireCallbacks()
         XCTAssertEqual(completionCount, 2)
     }
+  
+  func testRetrieveProductsInfo_when_calledConcurrentlyInDifferentThreads_then_eachcompletionCalledOnce_noCrashes() {
+    let requestBuilder = TestInAppProductRequestBuilder()
+    let productInfoController = ProductsInfoController(inAppProductRequestBuilder: requestBuilder)
+    
+    var completionCallbackCount = 0
+    
+    // Create the expectation not to let the test finishes before the other threads complete
+    let expectation = XCTestExpectation(description: "Expect downloads of product informations")
+    
+    // Create the dispatch group to let the test verifies the assert only when
+    // everything else finishes.
+    let group = DispatchGroup()
+    
+    // Dispatch a request for every product in a different thread
+    for product in testProducts {
+      DispatchQueue.global().async {
+        group.enter()
+        productInfoController.retrieveProductsInfo([product]) { _ in
+          completionCallbackCount += 1
+          group.leave()
+        }
+      }
+    }
+    DispatchQueue.global().asyncAfter(deadline: .now()+0.1) {
+      requestBuilder.fireCallbacks()
+    }
+    // Fullfil the expectation when every thread finishes
+    group.notify(queue: DispatchQueue.global()) {
+      
+      XCTAssertEqual(completionCallbackCount, self.testProducts.count)
+      expectation.fulfill()
+    }
+    
+    wait(for: [expectation], timeout: 10.0)
+  }
 }