diff --git a/CHANGELOG.md b/CHANGELOG.md index 30a58b7c..865eb6df 100644 --- a/CHANGELOG.md +++ b/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) diff --git a/Package.swift b/Package.swift index 83d5bbb8..b613332a 100644 --- a/Package.swift +++ b/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"]) ] ) diff --git a/Sources/SwiftyStoreKit/ProductsInfoController.swift b/Sources/SwiftyStoreKit/ProductsInfoController.swift index 591e26a1..0ba027bd 100644 --- a/Sources/SwiftyStoreKit/ProductsInfoController.swift +++ b/Sources/SwiftyStoreKit/ProductsInfoController.swift @@ -44,8 +44,12 @@ class ProductsInfoController: NSObject { } let inAppProductRequestBuilder: InAppProductRequestBuilder + + private var spinLock: OSSpinLock + init(inAppProductRequestBuilder: InAppProductRequestBuilder = InAppProductQueryRequestBuilder()) { self.inAppProductRequestBuilder = inAppProductRequestBuilder + self.spinLock = OSSpinLock() } // As we can have multiple inflight requests, we store them in a dictionary by product ids @@ -53,10 +57,18 @@ class ProductsInfoController: NSObject { @discardableResult func retrieveProductsInfo(_ productIds: Set, completion: @escaping (RetrieveResults) -> Void) -> InAppProductRequest { + OSSpinLockLock(&self.spinLock) + defer { + OSSpinLockUnlock(&self.spinLock) + } if inflightRequests[productIds] == nil { let request = inAppProductRequestBuilder.request(productIds: productIds) { results in - + OSSpinLockLock(&self.spinLock) + defer { + OSSpinLockUnlock(&self.spinLock) + } + if let query = self.inflightRequests[productIds] { for completion in query.completionHandlers { completion(results) diff --git a/Sources/SwiftyStoreKit/SKProductDiscount+LocalizedPrice.swift b/Sources/SwiftyStoreKit/SKProductDiscount+LocalizedPrice.swift index eddb7b7a..bf3ee7bd 100644 --- a/Sources/SwiftyStoreKit/SKProductDiscount+LocalizedPrice.swift +++ b/Sources/SwiftyStoreKit/SKProductDiscount+LocalizedPrice.swift @@ -60,4 +60,3 @@ public extension SKProductDiscount { } } - diff --git a/SwiftyStoreKit.xcodeproj/project.pbxproj b/SwiftyStoreKit.xcodeproj/project.pbxproj index 90d958dc..a2fe8043 100644 --- a/SwiftyStoreKit.xcodeproj/project.pbxproj +++ b/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 */; }; @@ -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; diff --git a/SwiftyStoreKit.xcodeproj/xcshareddata/xcschemes/SwiftyStoreKit-iOS-Demo.xcscheme b/SwiftyStoreKit.xcodeproj/xcshareddata/xcschemes/SwiftyStoreKit-iOS-Demo.xcscheme index f6475fd5..e2cf0182 100644 --- a/SwiftyStoreKit.xcodeproj/xcshareddata/xcschemes/SwiftyStoreKit-iOS-Demo.xcscheme +++ b/SwiftyStoreKit.xcodeproj/xcshareddata/xcschemes/SwiftyStoreKit-iOS-Demo.xcscheme @@ -1,6 +1,6 @@ + + + + @@ -39,17 +48,6 @@ - - - - - - - - - - - - - - - - + + - - - - - - - - - - + + - - - - - - - - - - [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 diff --git a/Tests/SwiftyStoreKitTests/Info.plist b/Tests/SwiftyStoreKitTests/Info-Tests.plist similarity index 100% rename from Tests/SwiftyStoreKitTests/Info.plist rename to Tests/SwiftyStoreKitTests/Info-Tests.plist diff --git a/Tests/SwiftyStoreKitTests/ProductsInfoControllerTests.swift b/Tests/SwiftyStoreKitTests/ProductsInfoControllerTests.swift index 982bded6..5d4de47d 100644 --- a/Tests/SwiftyStoreKitTests/ProductsInfoControllerTests.swift +++ b/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, 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 = ["com.iap.purchase1"] + // Set of in app purchases to ask in different threads + let testProducts: Set = ["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) + } }