From c98e90b518eef4e0728126115ef4973e33c110c9 Mon Sep 17 00:00:00 2001 From: Ivan <6350992+bivant@users.noreply.github.com> Date: Thu, 19 May 2022 22:58:00 +0300 Subject: [PATCH 1/4] Fix demo compilation error after b3f2601196a87569d463283c3a582ddcc7fccf0b - Merge pull request #652 from azouts/master Communicate deferred transactions to the app --- SwiftyStoreKit-iOS-Demo/ViewController.swift | 2 ++ SwiftyStoreKit-macOS-Demo/ViewController.swift | 2 ++ 2 files changed, 4 insertions(+) diff --git a/SwiftyStoreKit-iOS-Demo/ViewController.swift b/SwiftyStoreKit-iOS-Demo/ViewController.swift index fa640325..a690403a 100644 --- a/SwiftyStoreKit-iOS-Demo/ViewController.swift +++ b/SwiftyStoreKit-iOS-Demo/ViewController.swift @@ -279,6 +279,8 @@ extension ViewController { case .success(let purchase): print("Purchase Success: \(purchase.productId)") return nil + case .deferred(purchase: _): + return alertWithTitle("Purchase deferred", message: "The purchase deferred") case .error(let error): print("Purchase Failed: \(error)") switch error.code { diff --git a/SwiftyStoreKit-macOS-Demo/ViewController.swift b/SwiftyStoreKit-macOS-Demo/ViewController.swift index 071c6d9f..d0eaf729 100644 --- a/SwiftyStoreKit-macOS-Demo/ViewController.swift +++ b/SwiftyStoreKit-macOS-Demo/ViewController.swift @@ -193,6 +193,8 @@ extension ViewController { case .success(let purchase): print("Purchase Success: \(purchase.productId)") return alertWithTitle("Thank You", message: "Purchase completed") + case .deferred(purchase: _): + return alertWithTitle("Purchase deferred", message: "The purchase deferred") case .error(let error): print("Purchase Failed: \(error)") switch error.code { From 219a4c71bbb3f8b11382f0a48cd6a7635be8477b Mon Sep 17 00:00:00 2001 From: Ivan <6350992+bivant@users.noreply.github.com> Date: Thu, 19 May 2022 23:17:01 +0300 Subject: [PATCH 2/4] =?UTF-8?q?Fix=20crash=20on=20concurrent=20RetrievePro?= =?UTF-8?q?ductsInfo.=20Swift=E2=80=99s=20ownership=20rule=20is=20that=20a?= =?UTF-8?q?=20value=20can=20be=20simultaneously=20accessed=20by=20multiple?= =?UTF-8?q?=20readers=20or=20a=20single=20writer=20-=20https://developer.a?= =?UTF-8?q?pple.com/forums/thread/682446=3FanswerId=3D678850022#678850022?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SwiftyStoreKit/ProductsInfoController.swift | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftyStoreKit/ProductsInfoController.swift b/Sources/SwiftyStoreKit/ProductsInfoController.swift index e54d065f..49f0497f 100644 --- a/Sources/SwiftyStoreKit/ProductsInfoController.swift +++ b/Sources/SwiftyStoreKit/ProductsInfoController.swift @@ -49,7 +49,20 @@ class ProductsInfoController: NSObject { } // As we can have multiple inflight requests, we store them in a dictionary by product ids - private var inflightRequests: [Set: InAppProductQuery] = [:] + private var _inflightRequests: [Set: InAppProductQuery] = [:] + private let requestsQueue = DispatchQueue(label: "inflightRequestsQueue", attributes: .concurrent) + private var inflightRequests: [Set: InAppProductQuery] { + get { + requestsQueue.sync { + _inflightRequests + } + } + set { + requestsQueue.sync(flags: .barrier) { + self._inflightRequests = newValue + } + } + } @discardableResult func retrieveProductsInfo(_ productIds: Set, completion: @escaping (RetrieveResults) -> Void) -> InAppProductRequest { From baa344cdd5ec4780c8558772782e074ddc179012 Mon Sep 17 00:00:00 2001 From: Ivan <6350992+bivant@users.noreply.github.com> Date: Thu, 19 May 2022 23:23:28 +0300 Subject: [PATCH 3/4] Fix crash on concurrent RetrieveProductsInfo test. Wait for async retrieveProductsInfo execution before fireCallbacks() by DispatchGroup - groupFire --- .../ProductsInfoControllerTests.swift | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/Tests/SwiftyStoreKitTests/ProductsInfoControllerTests.swift b/Tests/SwiftyStoreKitTests/ProductsInfoControllerTests.swift index 3343be52..99ccc26f 100644 --- a/Tests/SwiftyStoreKitTests/ProductsInfoControllerTests.swift +++ b/Tests/SwiftyStoreKitTests/ProductsInfoControllerTests.swift @@ -54,11 +54,21 @@ class TestInAppProductRequest: InAppProductRequest { class TestInAppProductRequestBuilder: InAppProductRequestBuilder { - var requests: [ TestInAppProductRequest ] = [] + private var _requests: [TestInAppProductRequest] = [] + private let requestsQueue = DispatchQueue(label: "builderRequestsQueue", attributes: .concurrent) + var requests: [ TestInAppProductRequest ] { + get { + requestsQueue.sync { + _requests + } + } + } func request(productIds: Set, callback: @escaping InAppProductRequestCallback) -> InAppProductRequest { let request = TestInAppProductRequest(productIds: productIds, callback: callback) - requests.append(request) + requestsQueue.sync(flags: .barrier) { + _requests.append(request) + } return request } @@ -66,7 +76,7 @@ class TestInAppProductRequestBuilder: InAppProductRequestBuilder { requests.forEach { $0.fireCallback() } - requests = [] + _requests = [] } } @@ -143,25 +153,28 @@ class ProductsInfoControllerTests: XCTestCase { // 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 + // Create the dispatch groups to let the test verifies the assert only when // everything else finishes. - let group = DispatchGroup() + let groupCompletion = DispatchGroup() + let groupFire = DispatchGroup() // Dispatch a request for every product in a different thread for product in testProducts { + groupCompletion.enter() + groupFire.enter() DispatchQueue.global().async { - group.enter() productInfoController.retrieveProductsInfo([product]) { _ in completionCallbackCount += 1 - group.leave() + groupCompletion.leave() } + groupFire.leave() } } - DispatchQueue.global().asyncAfter(deadline: .now()+0.1) { + groupFire.notify(queue: DispatchQueue.global()) { requestBuilder.fireCallbacks() } // Fullfil the expectation when every thread finishes - group.notify(queue: DispatchQueue.global()) { + groupCompletion.notify(queue: DispatchQueue.global()) { XCTAssertEqual(completionCallbackCount, self.testProducts.count) expectation.fulfill() From 72d7e962a22ab896d65a1fb2791e0f78749484de Mon Sep 17 00:00:00 2001 From: Ivan <6350992+bivant@users.noreply.github.com> Date: Tue, 17 Jan 2023 00:51:29 +0300 Subject: [PATCH 4/4] Rename _inflightRequests by request from PR review --- Sources/SwiftyStoreKit/ProductsInfoController.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftyStoreKit/ProductsInfoController.swift b/Sources/SwiftyStoreKit/ProductsInfoController.swift index 49f0497f..32371a03 100644 --- a/Sources/SwiftyStoreKit/ProductsInfoController.swift +++ b/Sources/SwiftyStoreKit/ProductsInfoController.swift @@ -49,17 +49,17 @@ class ProductsInfoController: NSObject { } // As we can have multiple inflight requests, we store them in a dictionary by product ids - private var _inflightRequests: [Set: InAppProductQuery] = [:] + private var inflightRequestsStorage: [Set: InAppProductQuery] = [:] private let requestsQueue = DispatchQueue(label: "inflightRequestsQueue", attributes: .concurrent) private var inflightRequests: [Set: InAppProductQuery] { get { requestsQueue.sync { - _inflightRequests + inflightRequestsStorage } } set { requestsQueue.sync(flags: .barrier) { - self._inflightRequests = newValue + inflightRequestsStorage = newValue } } }