From 34ef0d4783cc8d0fe8eeb8c561c527c9aeadeae0 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Mon, 30 Sep 2019 11:17:34 +0200 Subject: [PATCH 01/20] feat: add failing test for ProductInfoController.inflightRequest - crash --- .../ProductsInfoControllerTests.swift | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/SwiftyStoreKitTests/ProductsInfoControllerTests.swift b/SwiftyStoreKitTests/ProductsInfoControllerTests.swift index 64a92106..637b2ebc 100644 --- a/SwiftyStoreKitTests/ProductsInfoControllerTests.swift +++ b/SwiftyStoreKitTests/ProductsInfoControllerTests.swift @@ -23,6 +23,7 @@ // THE SOFTWARE. import XCTest +import Foundation @testable import SwiftyStoreKit class TestInAppProductRequest: InAppProductRequest { @@ -50,8 +51,14 @@ 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 { + 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 +75,36 @@ class TestInAppProductRequestBuilder: InAppProductRequestBuilder { class ProductsInfoControllerTests: XCTestCase { let sampleProductIdentifiers: Set = ["com.iap.purchase1"] + 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", + "com.iap.purchase11", + "com.iap.purchase12", + "com.iap.purchase13", + "com.iap.purchase14", + "com.iap.purchase15", + "com.iap.purchase16", + "com.iap.purchase17", + "com.iap.purchase18", + "com.iap.purchase19", + "com.iap.purchase20", + "com.iap.purchase21", + "com.iap.purchase22", + "com.iap.purchase23", + "com.iap.purchase24", + "com.iap.purchase25", + "com.iap.purchase26", + "com.iap.purchase27", + "com.iap.purchase28", + "com.iap.purchase29", + "com.iap.purchase30",] func testRetrieveProductsInfo_when_calledOnce_then_completionCalledOnce() { @@ -117,4 +154,30 @@ class ProductsInfoControllerTests: XCTestCase { requestBuilder.fireCallbacks() XCTAssertEqual(completionCount, 2) } + + func testRetrieveProductsInfo_when_calledConcurrentlyInDifferentThreads_then_eachcompletionCalledOnce_noCrashes() { + let requestBuilder = TestInAppProductRequestBuilder() + let productInfoController = ProductsInfoController(inAppProductRequestBuilder: requestBuilder) + + var completionCalledSet: Set = [] + + let expectation = XCTestExpectation(description: "Expect downloads of product informations") + let group = DispatchGroup() + + for product in testProducts { + DispatchQueue.global().async { + group.enter() + productInfoController.retrieveProductsInfo([product]) { _ in + completionCalledSet.insert(product) + group.leave() + } + } + } + + group.notify(queue: DispatchQueue.global()) { + expectation.fulfill() + } + + XCTAssertEqual(completionCalledSet, testProducts) + } } From 511f4b1d45f6727c7f85dc58fc87b25bd60c4dd9 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Mon, 30 Sep 2019 11:20:59 +0200 Subject: [PATCH 02/20] fix: reduce the amount of products --- .../ProductsInfoControllerTests.swift | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/SwiftyStoreKitTests/ProductsInfoControllerTests.swift b/SwiftyStoreKitTests/ProductsInfoControllerTests.swift index 637b2ebc..ac8131e1 100644 --- a/SwiftyStoreKitTests/ProductsInfoControllerTests.swift +++ b/SwiftyStoreKitTests/ProductsInfoControllerTests.swift @@ -84,27 +84,7 @@ class ProductsInfoControllerTests: XCTestCase { "com.iap.purchase07", "com.iap.purchase08", "com.iap.purchase09", - "com.iap.purchase10", - "com.iap.purchase11", - "com.iap.purchase12", - "com.iap.purchase13", - "com.iap.purchase14", - "com.iap.purchase15", - "com.iap.purchase16", - "com.iap.purchase17", - "com.iap.purchase18", - "com.iap.purchase19", - "com.iap.purchase20", - "com.iap.purchase21", - "com.iap.purchase22", - "com.iap.purchase23", - "com.iap.purchase24", - "com.iap.purchase25", - "com.iap.purchase26", - "com.iap.purchase27", - "com.iap.purchase28", - "com.iap.purchase29", - "com.iap.purchase30",] + "com.iap.purchase10"] func testRetrieveProductsInfo_when_calledOnce_then_completionCalledOnce() { From 947382fcb91d136f0e5b59af012c4a30d335a805 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Mon, 30 Sep 2019 11:21:13 +0200 Subject: [PATCH 03/20] docs: add comments to the test --- SwiftyStoreKitTests/ProductsInfoControllerTests.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/SwiftyStoreKitTests/ProductsInfoControllerTests.swift b/SwiftyStoreKitTests/ProductsInfoControllerTests.swift index ac8131e1..77d5d7d2 100644 --- a/SwiftyStoreKitTests/ProductsInfoControllerTests.swift +++ b/SwiftyStoreKitTests/ProductsInfoControllerTests.swift @@ -54,6 +54,7 @@ class TestInAppProductRequestBuilder: InAppProductRequestBuilder { 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) @@ -75,6 +76,7 @@ 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", @@ -141,9 +143,14 @@ class ProductsInfoControllerTests: XCTestCase { var completionCalledSet: Set = [] + // 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() @@ -154,6 +161,7 @@ class ProductsInfoControllerTests: XCTestCase { } } + // Fullfil the expectation when every thread finishes group.notify(queue: DispatchQueue.global()) { expectation.fulfill() } From 76e83e0182fbda21f4619d83d41844ecfb0f3977 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Mon, 30 Sep 2019 11:53:54 +0200 Subject: [PATCH 04/20] fix: make the test works properly, calling the fireCallbakcs method. --- .../ProductsInfoControllerTests.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/SwiftyStoreKitTests/ProductsInfoControllerTests.swift b/SwiftyStoreKitTests/ProductsInfoControllerTests.swift index 77d5d7d2..0d356a34 100644 --- a/SwiftyStoreKitTests/ProductsInfoControllerTests.swift +++ b/SwiftyStoreKitTests/ProductsInfoControllerTests.swift @@ -141,7 +141,7 @@ class ProductsInfoControllerTests: XCTestCase { let requestBuilder = TestInAppProductRequestBuilder() let productInfoController = ProductsInfoController(inAppProductRequestBuilder: requestBuilder) - var completionCalledSet: Set = [] + 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") @@ -155,17 +155,21 @@ class ProductsInfoControllerTests: XCTestCase { DispatchQueue.global().async { group.enter() productInfoController.retrieveProductsInfo([product]) { _ in - completionCalledSet.insert(product) + completionCallbackCount += 1 group.leave() } } } - + DispatchQueue.global().asyncAfter(deadline: .now() + 1) { + requestBuilder.fireCallbacks() + } // Fullfil the expectation when every thread finishes group.notify(queue: DispatchQueue.global()) { + + XCTAssertEqual(completionCallbackCount, self.testProducts.count) expectation.fulfill() } - XCTAssertEqual(completionCalledSet, testProducts) + wait(for: [expectation], timeout: 10.0) } } From 72f88b03ca567399c1ee1360bb0f02a28ed74720 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Mon, 30 Sep 2019 11:54:19 +0200 Subject: [PATCH 05/20] fix: implement OS agnostic locking. --- SwiftyStoreKit/ProductsInfoController.swift | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/SwiftyStoreKit/ProductsInfoController.swift b/SwiftyStoreKit/ProductsInfoController.swift index cb5bff48..33ebbfe9 100644 --- a/SwiftyStoreKit/ProductsInfoController.swift +++ b/SwiftyStoreKit/ProductsInfoController.swift @@ -44,8 +44,18 @@ class ProductsInfoController: NSObject { } let inAppProductRequestBuilder: InAppProductRequestBuilder + + private var unfairLock: os_unfair_lock_s! + private var spinLock: OSSpinLock! + init(inAppProductRequestBuilder: InAppProductRequestBuilder = InAppProductQueryRequestBuilder()) { self.inAppProductRequestBuilder = inAppProductRequestBuilder + if #available(iOSApplicationExtension 10.0, *) { + self.unfairLock = os_unfair_lock() + } else { + self.spinLock = OSSpinLock() + } + super.init() } // As we can have multiple inflight requests, we store them in a dictionary by product ids @@ -72,4 +82,22 @@ class ProductsInfoController: NSObject { inflightRequests[productIds]!.completionHandlers.append(completion) } } + + + + private func lock() { + if #available(iOSApplicationExtension 10.0, *) { + os_unfair_lock_lock(&self.unfairLock) + } else { + OSSpinLockLock(&self.spinLock) + } + } + + private func unlock() { + if #available(iOSApplicationExtension 10.0, *) { + os_unfair_lock_unlock(&self.unfairLock) + } else { + OSSpinLockUnlock(&self.spinLock) + } + } } From d3ed588d9858fa11c652597cb712242e671e52cb Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Mon, 30 Sep 2019 11:54:49 +0200 Subject: [PATCH 06/20] =?UTF-8?q?fix:=20lock=20the=20retrieveProductsInfo?= =?UTF-8?q?=20so=20that=20it=E2=80=99s=20thread=20safe.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SwiftyStoreKit/ProductsInfoController.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/SwiftyStoreKit/ProductsInfoController.swift b/SwiftyStoreKit/ProductsInfoController.swift index 33ebbfe9..28853feb 100644 --- a/SwiftyStoreKit/ProductsInfoController.swift +++ b/SwiftyStoreKit/ProductsInfoController.swift @@ -62,10 +62,17 @@ class ProductsInfoController: NSObject { private var inflightRequests: [Set: InAppProductQuery] = [:] func retrieveProductsInfo(_ productIds: Set, completion: @escaping (RetrieveResults) -> Void) { - + self.lock() + defer { + self.unlock() + } if inflightRequests[productIds] == nil { let request = inAppProductRequestBuilder.request(productIds: productIds) { results in - + self.lock() + defer { + self.unlock() + } + if let query = self.inflightRequests[productIds] { for completion in query.completionHandlers { completion(results) From 23f0f2d724a010de0145adaf5c54bb2b6de790ee Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Mon, 30 Sep 2019 13:47:00 +0200 Subject: [PATCH 07/20] docs: add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) 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) From f5d9858bc22a6d80f29abd4b022e00e7831603cb Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Mon, 30 Sep 2019 14:19:16 +0200 Subject: [PATCH 08/20] fix: remove api available only on macos and only from version 10 --- SwiftyStoreKit/ProductsInfoController.swift | 38 ++++----------------- 1 file changed, 7 insertions(+), 31 deletions(-) diff --git a/SwiftyStoreKit/ProductsInfoController.swift b/SwiftyStoreKit/ProductsInfoController.swift index 28853feb..7cf492af 100644 --- a/SwiftyStoreKit/ProductsInfoController.swift +++ b/SwiftyStoreKit/ProductsInfoController.swift @@ -44,33 +44,27 @@ class ProductsInfoController: NSObject { } let inAppProductRequestBuilder: InAppProductRequestBuilder - - private var unfairLock: os_unfair_lock_s! - private var spinLock: OSSpinLock! + + private var spinLock: OSSpinLock init(inAppProductRequestBuilder: InAppProductRequestBuilder = InAppProductQueryRequestBuilder()) { self.inAppProductRequestBuilder = inAppProductRequestBuilder - if #available(iOSApplicationExtension 10.0, *) { - self.unfairLock = os_unfair_lock() - } else { - self.spinLock = OSSpinLock() - } - super.init() + self.spinLock = OSSpinLock() } // As we can have multiple inflight requests, we store them in a dictionary by product ids private var inflightRequests: [Set: InAppProductQuery] = [:] func retrieveProductsInfo(_ productIds: Set, completion: @escaping (RetrieveResults) -> Void) { - self.lock() + OSSpinLockLock(&self.spinLock) defer { - self.unlock() + OSSpinLockUnlock(&self.spinLock) } if inflightRequests[productIds] == nil { let request = inAppProductRequestBuilder.request(productIds: productIds) { results in - self.lock() + OSSpinLockLock(&self.spinLock) defer { - self.unlock() + OSSpinLockUnlock(&self.spinLock) } if let query = self.inflightRequests[productIds] { @@ -89,22 +83,4 @@ class ProductsInfoController: NSObject { inflightRequests[productIds]!.completionHandlers.append(completion) } } - - - - private func lock() { - if #available(iOSApplicationExtension 10.0, *) { - os_unfair_lock_lock(&self.unfairLock) - } else { - OSSpinLockLock(&self.spinLock) - } - } - - private func unlock() { - if #available(iOSApplicationExtension 10.0, *) { - os_unfair_lock_unlock(&self.unfairLock) - } else { - OSSpinLockUnlock(&self.spinLock) - } - } } From b8ee2d354e78bfd238379fb1695c72122a060ba4 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Mon, 30 Sep 2019 14:44:04 +0200 Subject: [PATCH 09/20] fix: reduce the wait time to fire the callbacks --- SwiftyStoreKitTests/ProductsInfoControllerTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SwiftyStoreKitTests/ProductsInfoControllerTests.swift b/SwiftyStoreKitTests/ProductsInfoControllerTests.swift index 0d356a34..5b2e73a9 100644 --- a/SwiftyStoreKitTests/ProductsInfoControllerTests.swift +++ b/SwiftyStoreKitTests/ProductsInfoControllerTests.swift @@ -160,7 +160,7 @@ class ProductsInfoControllerTests: XCTestCase { } } } - DispatchQueue.global().asyncAfter(deadline: .now() + 1) { + DispatchQueue.global().asyncAfter(deadline: .now()+0.1) { requestBuilder.fireCallbacks() } // Fullfil the expectation when every thread finishes From 6aca8f0f2402ff9788405e18b0aa6dfddb9e2539 Mon Sep 17 00:00:00 2001 From: Stefanos Zachariadis Date: Mon, 20 Jul 2020 16:15:10 +0100 Subject: [PATCH 10/20] make watchos SKError.code public so that it's accessible outside of the framework --- Sources/SwiftyStoreKit/OS.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 } From 38d34dad275cc90706f4daaadf401579b33737ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B5=D0=BD=D0=B8=D1=81=20=D0=90=D0=BD=D0=B4=D1=80?= =?UTF-8?q?=D0=B5=D0=B8=CC=86=D1=87=D1=83=D0=BA?= Date: Wed, 22 Jul 2020 16:13:19 +0300 Subject: [PATCH 11/20] Make `SubscriptionType` Hashable. --- Sources/SwiftyStoreKit/SwiftyStoreKit+Types.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) } From 6004597112e24688bf6b1dd365193eee6550cac2 Mon Sep 17 00:00:00 2001 From: Stefanos Zachariadis Date: Sat, 15 Aug 2020 16:50:01 +0300 Subject: [PATCH 12/20] add support for ios14 entitlement revocation --- .../PaymentQueueController.swift | 20 ++++++++++++++++++- Sources/SwiftyStoreKit/SwiftyStoreKit.swift | 13 ++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftyStoreKit/PaymentQueueController.swift b/Sources/SwiftyStoreKit/PaymentQueueController.swift index 758dafd2..43df1bd3 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,10 @@ class PaymentQueueController: NSObject, SKPaymentTransactionObserver { paymentsController.append(payment) } + func onEntitlementRevocation(_ revocation: EntitlementRevocation) { + self.entitlementRevocation = revocation + } + func restorePurchases(_ restorePurchases: RestorePurchases) { assertCompleteTransactionsWasCalled() @@ -233,6 +247,10 @@ 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/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 /// From 3e7d70d9bdcc3b84194a26ef4caa66b8ecbf422e Mon Sep 17 00:00:00 2001 From: Stefanos Zachariadis Date: Sat, 15 Aug 2020 17:30:30 +0300 Subject: [PATCH 13/20] add entitlement revocation only once --- Sources/SwiftyStoreKit/PaymentQueueController.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/SwiftyStoreKit/PaymentQueueController.swift b/Sources/SwiftyStoreKit/PaymentQueueController.swift index 43df1bd3..872c8fea 100644 --- a/Sources/SwiftyStoreKit/PaymentQueueController.swift +++ b/Sources/SwiftyStoreKit/PaymentQueueController.swift @@ -248,6 +248,11 @@ class PaymentQueueController: NSObject, SKPaymentTransactionObserver { } func paymentQueue(_ queue: SKPaymentQueue, didRevokeEntitlementsForProductIdentifiers productIdentifiers: [String]) { + guard entitlementRevocation == nil else { + print("SwiftyStoreKit.onEntitlementRevocation() should only be called once when the app launches. Ignoring this call") + return + } + self.entitlementRevocation?.callback(productIdentifiers) } From f0a4198865d8747afd9dd626a94e594aa4e91c32 Mon Sep 17 00:00:00 2001 From: Stefanos Zachariadis Date: Sat, 15 Aug 2020 18:15:15 +0300 Subject: [PATCH 14/20] add entitlement revocation only once --- Sources/SwiftyStoreKit/PaymentQueueController.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftyStoreKit/PaymentQueueController.swift b/Sources/SwiftyStoreKit/PaymentQueueController.swift index 872c8fea..a721ee02 100644 --- a/Sources/SwiftyStoreKit/PaymentQueueController.swift +++ b/Sources/SwiftyStoreKit/PaymentQueueController.swift @@ -156,6 +156,11 @@ class PaymentQueueController: NSObject, SKPaymentTransactionObserver { } 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 } @@ -248,10 +253,6 @@ class PaymentQueueController: NSObject, SKPaymentTransactionObserver { } func paymentQueue(_ queue: SKPaymentQueue, didRevokeEntitlementsForProductIdentifiers productIdentifiers: [String]) { - guard entitlementRevocation == nil else { - print("SwiftyStoreKit.onEntitlementRevocation() should only be called once when the app launches. Ignoring this call") - return - } self.entitlementRevocation?.callback(productIdentifiers) } From f6d7745a67411a137f2a3102e5df1616f5f162f0 Mon Sep 17 00:00:00 2001 From: Iulian Onofrei <6d0847b9@opayq.com> Date: Wed, 9 Sep 2020 12:45:47 +0300 Subject: [PATCH 15/20] Fix typo in code comment --- Sources/SwiftyStoreKit/PaymentQueueController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftyStoreKit/PaymentQueueController.swift b/Sources/SwiftyStoreKit/PaymentQueueController.swift index a721ee02..bce45ce0 100644 --- a/Sources/SwiftyStoreKit/PaymentQueueController.swift +++ b/Sources/SwiftyStoreKit/PaymentQueueController.swift @@ -225,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. * From dd0e6f21c3c51a2b1fee86eea0eebd52f10e20ec Mon Sep 17 00:00:00 2001 From: martino-dot <63510642+martino-dot@users.noreply.github.com> Date: Sat, 19 Sep 2020 21:37:00 -0500 Subject: [PATCH 16/20] Added Botcher to apps using SwiftStoreKit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8a5cef0c..ad15667d 100644 --- a/README.md +++ b/README.md @@ -798,6 +798,7 @@ 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 A full list of apps is published [on AppSight](https://www.appsight.io/sdk/574154). From 02163c35c9418bd1692fb7e19eacbb977cd573a0 Mon Sep 17 00:00:00 2001 From: Samuel Spencer Date: Mon, 21 Sep 2020 16:52:24 -0500 Subject: [PATCH 17/20] Update Package.swift Bumped minimum iOS deployment version from 8 to 9 in order to comply with Xcode 12's minimum deployment range. Fixes compiler warning when installing via SPM. Fixes #578 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index b613332a..61dc3b3e 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription let package = Package( name: "SwiftyStoreKit", - platforms: [.iOS("8.0"), .macOS("10.10"), .tvOS("9.0"), .watchOS("6.2")], + platforms: [.iOS("9.0"), .macOS("10.10"), .tvOS("9.0"), .watchOS("6.2")], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( From 24a58abb763c8041ecbc6326cad9396bf24aa925 Mon Sep 17 00:00:00 2001 From: Muukii Date: Tue, 29 Sep 2020 06:10:42 +0900 Subject: [PATCH 18/20] Call completion closure on completed request --- .../InAppProductQueryRequest.swift | 18 +++++++++++++++--- .../ProductsInfoController.swift | 12 ++++++++++++ SwiftyStoreKit.xcodeproj/project.pbxproj | 4 ++-- 3 files changed, 29 insertions(+), 5 deletions(-) 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/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/SwiftyStoreKit.xcodeproj/project.pbxproj b/SwiftyStoreKit.xcodeproj/project.pbxproj index 90d958dc..63e44eaa 100644 --- a/SwiftyStoreKit.xcodeproj/project.pbxproj +++ b/SwiftyStoreKit.xcodeproj/project.pbxproj @@ -200,7 +200,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 +208,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; }; From b2c030b9398944d1a37e2a972c485998936f1b88 Mon Sep 17 00:00:00 2001 From: Dominic <66052711+d-rr@users.noreply.github.com> Date: Wed, 21 Oct 2020 08:33:30 +0200 Subject: [PATCH 19/20] Adds Hashr to showcase apps --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8a5cef0c..3e164c5a 100644 --- a/README.md +++ b/README.md @@ -798,6 +798,7 @@ 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 +* [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). From 70761d29fceb2c0d68a93dd3f26c81db4c0321ea Mon Sep 17 00:00:00 2001 From: Sam Spencer Date: Mon, 4 Jan 2021 14:33:20 -0500 Subject: [PATCH 20/20] Update ProductsInfoController.swift Reverted addition of OSSpinLock, which is both deprecated and causes hangs when used incorrectly (which is easy and happens frequently). --- .../SwiftyStoreKit/ProductsInfoController.swift | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/Sources/SwiftyStoreKit/ProductsInfoController.swift b/Sources/SwiftyStoreKit/ProductsInfoController.swift index aa70b877..0478439d 100644 --- a/Sources/SwiftyStoreKit/ProductsInfoController.swift +++ b/Sources/SwiftyStoreKit/ProductsInfoController.swift @@ -44,12 +44,8 @@ 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 @@ -57,18 +53,10 @@ 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)