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 f180bb44..61dc3b3e 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/README.md b/README.md index 8a5cef0c..aec34b18 100644 --- a/README.md +++ b/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). diff --git a/Sources/SwiftyStoreKit/InAppProductQueryRequest.swift b/Sources/SwiftyStoreKit/InAppProductQueryRequest.swift index 97c9f889..3494f27e 100644 --- a/Sources/SwiftyStoreKit/InAppProductQueryRequest.swift +++ b/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(response.products) let invalidProductIDs = Set(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) { diff --git a/Sources/SwiftyStoreKit/OS.swift b/Sources/SwiftyStoreKit/OS.swift index 69c54ab8..46290585 100644 --- a/Sources/SwiftyStoreKit/OS.swift +++ b/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 } diff --git a/Sources/SwiftyStoreKit/PaymentQueueController.swift b/Sources/SwiftyStoreKit/PaymentQueueController.swift index 758dafd2..bce45ce0 100644 --- a/Sources/SwiftyStoreKit/PaymentQueueController.swift +++ b/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]) { } diff --git a/Sources/SwiftyStoreKit/ProductsInfoController.swift b/Sources/SwiftyStoreKit/ProductsInfoController.swift index 591e26a1..0478439d 100644 --- a/Sources/SwiftyStoreKit/ProductsInfoController.swift +++ b/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 } } 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/Sources/SwiftyStoreKit/SwiftyStoreKit+Types.swift b/Sources/SwiftyStoreKit/SwiftyStoreKit+Types.swift index 8eea5c9b..addf9b78 100644 --- a/Sources/SwiftyStoreKit/SwiftyStoreKit+Types.swift +++ b/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) } diff --git a/Sources/SwiftyStoreKit/SwiftyStoreKit.swift b/Sources/SwiftyStoreKit/SwiftyStoreKit.swift index 7e51a14e..f7e2ba61 100644 --- a/Sources/SwiftyStoreKit/SwiftyStoreKit.swift +++ b/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 /// diff --git a/SwiftyStoreKit.xcodeproj/project.pbxproj b/SwiftyStoreKit.xcodeproj/project.pbxproj index 90d958dc..4385ef17 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 */; }; @@ -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; 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) + } }