Skip to content

Commit

Permalink
Merge pull request #259 from bizz84/bugfix/ProductsInfoController-mul…
Browse files Browse the repository at this point in the history
…tiple-completion-blocks

ProductsInfoController: Keep track of multiple completion blocks for the same request
  • Loading branch information
bizz84 authored Aug 21, 2017
2 parents 6dc903c + ca94438 commit 5cf1ba9
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 17 deletions.
4 changes: 4 additions & 0 deletions SwiftyStoreKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
65BB6CE81DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */; };
65BB6CE91DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */; };
65BB6CEA1DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */; };
65BF8E301F4AEEBA00CBFC00 /* ProductsInfoControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BF8E2F1F4AEEBA00CBFC00 /* ProductsInfoControllerTests.swift */; };
65CEF0F41ECC80D9007DC3B6 /* InAppReceiptVerificatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65CEF0F31ECC80D9007DC3B6 /* InAppReceiptVerificatorTests.swift */; };
65E9E0791ECADF5E005CF7B4 /* InAppReceiptVerificator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */; };
65E9E07A1ECADF5E005CF7B4 /* InAppReceiptVerificator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */; };
Expand Down Expand Up @@ -183,6 +184,7 @@
658A084B1E2EC5960074A98F /* PaymentQueueSpy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentQueueSpy.swift; sourceTree = "<group>"; };
65B8C9281EC0BE62009439D9 /* InAppReceiptTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppReceiptTests.swift; sourceTree = "<group>"; };
65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SwiftyStoreKit+Types.swift"; sourceTree = "<group>"; };
65BF8E2F1F4AEEBA00CBFC00 /* ProductsInfoControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsInfoControllerTests.swift; sourceTree = "<group>"; };
65CEF0F31ECC80D9007DC3B6 /* InAppReceiptVerificatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppReceiptVerificatorTests.swift; sourceTree = "<group>"; };
65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppReceiptVerificator.swift; sourceTree = "<group>"; };
65F70AC61E2ECBB300BF040D /* PaymentTransactionObserverFake.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentTransactionObserverFake.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -345,6 +347,7 @@
65F70AC61E2ECBB300BF040D /* PaymentTransactionObserverFake.swift */,
C3099C081E2FCE3A00392A54 /* TestProduct.swift */,
C3099C0A1E2FD13200392A54 /* TestPaymentTransaction.swift */,
65BF8E2F1F4AEEBA00CBFC00 /* ProductsInfoControllerTests.swift */,
);
path = SwiftyStoreKitTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -774,6 +777,7 @@
files = (
C3099C071E2FCDAA00392A54 /* PaymentsControllerTests.swift in Sources */,
650307F21E3163AA001332A4 /* RestorePurchasesControllerTests.swift in Sources */,
65BF8E301F4AEEBA00CBFC00 /* ProductsInfoControllerTests.swift in Sources */,
65CEF0F41ECC80D9007DC3B6 /* InAppReceiptVerificatorTests.swift in Sources */,
C3099C0B1E2FD13200392A54 /* TestPaymentTransaction.swift in Sources */,
65F70AC71E2ECBB300BF040D /* PaymentTransactionObserverFake.swift in Sources */,
Expand Down
22 changes: 11 additions & 11 deletions SwiftyStoreKit/InAppProductQueryRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,29 @@

import StoreKit

class InAppProductQueryRequest: NSObject, SKProductsRequestDelegate {
typealias InAppProductRequestCallback = (RetrieveResults) -> Void

typealias RequestCallback = (RetrieveResults) -> Void
private let callback: RequestCallback
protocol InAppProductRequest: class {
func start()
func cancel()
}

class InAppProductQueryRequest: NSObject, InAppProductRequest, SKProductsRequestDelegate {

private let callback: InAppProductRequestCallback
private let request: SKProductsRequest
// http://stackoverflow.com/questions/24011575/what-is-the-difference-between-a-weak-reference-and-an-unowned-reference

deinit {
request.delegate = nil
}
private init(productIds: Set<String>, callback: @escaping RequestCallback) {
init(productIds: Set<String>, callback: @escaping InAppProductRequestCallback) {

self.callback = callback
request = SKProductsRequest(productIdentifiers: productIds)
super.init()
request.delegate = self
}

class func startQuery(_ productIds: Set<String>, callback: @escaping RequestCallback) -> InAppProductQueryRequest {
let request = InAppProductQueryRequest(productIds: productIds, callback: callback)
request.start()
return request
}

func start() {
request.start()
}
Expand Down
46 changes: 40 additions & 6 deletions SwiftyStoreKit/ProductsInfoController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,51 @@
import Foundation
import StoreKit

protocol InAppProductRequestBuilder: class {
func request(productIds: Set<String>, callback: @escaping InAppProductRequestCallback) -> InAppProductRequest
}

class InAppProductQueryRequestBuilder: InAppProductRequestBuilder {

func request(productIds: Set<String>, callback: @escaping InAppProductRequestCallback) -> InAppProductRequest {
return InAppProductQueryRequest(productIds: productIds, callback: callback)
}
}

class ProductsInfoController: NSObject {

// As we can have multiple inflight queries and purchases, we store them in a dictionary by product id
private var inflightQueries: [Set<String>: InAppProductQueryRequest] = [:]
struct InAppProductQuery {
let request: InAppProductRequest
var completionHandlers: [InAppProductRequestCallback]
}

let inAppProductRequestBuilder: InAppProductRequestBuilder
init(inAppProductRequestBuilder: InAppProductRequestBuilder = InAppProductQueryRequestBuilder()) {
self.inAppProductRequestBuilder = inAppProductRequestBuilder
}

// As we can have multiple inflight requests, we store them in a dictionary by product ids
private var inflightRequests: [Set<String>: InAppProductQuery] = [:]

func retrieveProductsInfo(_ productIds: Set<String>, completion: @escaping (RetrieveResults) -> Void) {

inflightQueries[productIds] = InAppProductQueryRequest.startQuery(productIds) { result in

self.inflightQueries[productIds] = nil
completion(result)
if inflightRequests[productIds] == nil {
let request = inAppProductRequestBuilder.request(productIds: productIds) { results in

if let query = self.inflightRequests[productIds] {
for completion in query.completionHandlers {
completion(results)
}
self.inflightRequests[productIds] = nil
} else {
// should not get here, but if it does it seems reasonable to call the outer completion block
completion(results)
}
}
inflightRequests[productIds] = InAppProductQuery(request: request, completionHandlers: [completion])
request.start()
} else {
inflightRequests[productIds]!.completionHandlers.append(completion)
}
}
}
120 changes: 120 additions & 0 deletions SwiftyStoreKitTests/ProductsInfoControllerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//
// ProductsInfoControllerTests.swift
// SwiftyStoreKit
//
// Copyright (c) 2017 Andrea Bizzotto ([email protected])
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import XCTest
@testable import SwiftyStoreKit

class TestInAppProductRequest: InAppProductRequest {

private let productIds: Set<String>
private let callback: InAppProductRequestCallback

init(productIds: Set<String>, callback: @escaping InAppProductRequestCallback) {
self.productIds = productIds
self.callback = callback
}

func start() {

}
func cancel() {

}

func fireCallback() {
callback(RetrieveResults(retrievedProducts: [], invalidProductIDs: [], error: nil))
}
}

class TestInAppProductRequestBuilder: InAppProductRequestBuilder {

var requests: [ TestInAppProductRequest ] = []

func request(productIds: Set<String>, callback: @escaping InAppProductRequestCallback) -> InAppProductRequest {
let request = TestInAppProductRequest(productIds: productIds, callback: callback)
requests.append(request)
return request
}

func fireCallbacks() {
requests.forEach {
$0.fireCallback()
}
requests = []
}
}

class ProductsInfoControllerTests: XCTestCase {

let sampleProductIdentifiers: Set<String> = ["com.iap.purchase1"]

func testRetrieveProductsInfo_when_calledOnce_then_completionCalledOnce() {

let requestBuilder = TestInAppProductRequestBuilder()
let productInfoController = ProductsInfoController(inAppProductRequestBuilder: requestBuilder)

var completionCount = 0
productInfoController.retrieveProductsInfo(sampleProductIdentifiers) { _ in
completionCount += 1
}
requestBuilder.fireCallbacks()

XCTAssertEqual(completionCount, 1)
}

func testRetrieveProductsInfo_when_calledTwiceConcurrently_then_eachCompletionCalledOnce() {

let requestBuilder = TestInAppProductRequestBuilder()
let productInfoController = ProductsInfoController(inAppProductRequestBuilder: requestBuilder)

var completionCount = 0
productInfoController.retrieveProductsInfo(sampleProductIdentifiers) { _ in
completionCount += 1
}
productInfoController.retrieveProductsInfo(sampleProductIdentifiers) { _ in
completionCount += 1
}
requestBuilder.fireCallbacks()

XCTAssertEqual(completionCount, 2)
}
func testRetrieveProductsInfo_when_calledTwiceNotConcurrently_then_eachCompletionCalledOnce() {

let requestBuilder = TestInAppProductRequestBuilder()
let productInfoController = ProductsInfoController(inAppProductRequestBuilder: requestBuilder)

var completionCount = 0
productInfoController.retrieveProductsInfo(sampleProductIdentifiers) { _ in
completionCount += 1
}
requestBuilder.fireCallbacks()
XCTAssertEqual(completionCount, 1)

productInfoController.retrieveProductsInfo(sampleProductIdentifiers) { _ in
completionCount += 1
}
requestBuilder.fireCallbacks()
XCTAssertEqual(completionCount, 2)
}
}

0 comments on commit 5cf1ba9

Please sign in to comment.