Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ProductsInfoController: Keep track of multiple completion blocks for the same request #259

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 (bizz84@gmail.com)
//
// 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)
}
}