Browse Source

Merge pull request #23 from bizz84/feature/retrieve-multiple-products-at-once

Feature/retrieve multiple products at once
Andrea Bizzotto 9 years ago
parent
commit
14c467cf67

+ 10 - 7
README.md

@@ -18,15 +18,18 @@ SwiftyStoreKit is a lightweight In App Purchases framework for iOS 8.0+ and OSX
 <img src="https://github.com/bizz84/SwiftyStoreKit/raw/master/Screenshots/Preview.png" width="320">
 <img src="https://github.com/bizz84/SwiftyStoreKit/raw/master/Screenshots/Preview2.png" width="320">
 
-### Retrieve product info
+### Retrieve products info
 ```swift
-SwiftyStoreKit.retrieveProductInfo("com.musevisions.SwiftyStoreKit.Purchase1") { result in
-    switch result {
-    case .Success(let product):
-        let priceString = NSNumberFormatter.localizedStringFromNumber(product.price, numberStyle: .CurrencyStyle)
+SwiftyStoreKit.retrieveProductsInfo(["com.musevisions.SwiftyStoreKit.Purchase1"]) { result in
+    if let product = result.retrievedProducts.first {
+        let priceString = NSNumberFormatter.localizedStringFromNumber(product.price ?? 0, numberStyle: .CurrencyStyle)
         print("Product: \(product.localizedDescription), price: \(priceString)")
-    case .Error(let error):
-        print("Error: \(error)")
+    }
+    else if let invalidProductId = result.invalidProductIDs.first {
+        return alertWithTitle("Could not retrieve product info", message: "Invalid product identifier: \(invalidProductId)")
+    }
+    else {
+	     print("Error: \(result.error)")
     }
 }
 ```

+ 13 - 10
SwiftyStoreDemo/ViewController.swift

@@ -47,7 +47,7 @@ class ViewController: UIViewController {
     func getInfo(no: String) {
         
         NetworkActivityIndicatorManager.networkOperationStarted()
-        SwiftyStoreKit.retrieveProductInfo(AppBundleId + ".purchase" + no) { result in
+        SwiftyStoreKit.retrieveProductsInfo([AppBundleId + ".purchase" + no]) { result in
             NetworkActivityIndicatorManager.networkOperationFinished()
             
             self.showAlert(self.alertForProductRetrievalInfo(result))
@@ -119,14 +119,18 @@ extension ViewController {
         }
     }
 
-    func alertForProductRetrievalInfo(result: SwiftyStoreKit.RetrieveResult) -> UIAlertController {
+    func alertForProductRetrievalInfo(result: SwiftyStoreKit.RetrieveResults) -> UIAlertController {
         
-        switch result {
-        case .Success(let product):
+        if let product = result.retrievedProducts.first {
             let priceString = NSNumberFormatter.localizedStringFromNumber(product.price, numberStyle: .CurrencyStyle)
             return alertWithTitle(product.localizedTitle, message: "\(product.localizedDescription) - \(priceString)")
-        case .Error(let error):
-            return alertWithTitle("Could not retrieve product info", message: String(error))
+        }
+        else if let invalidProductId = result.invalidProductIDs.first {
+            return alertWithTitle("Could not retrieve product info", message: "Invalid product identifier: \(invalidProductId)")
+        }
+        else {
+            let errorString = result.error?.localizedDescription ?? "Unknown error. Please contact support"
+            return alertWithTitle("Could not retrieve product info", message: errorString)
         }
     }
 
@@ -139,13 +143,12 @@ extension ViewController {
             print("Purchase Failed: \(error)")
             switch error {
                 case .Failed(let error):
-                    if case ResponseError.RequestFailed(let internalError) = error where internalError.domain == SKErrorDomain {
-                        return alertWithTitle("Purchase failed", message: "Please check your Internet connection or try again later")
-                    }
-                    if (error as NSError).domain == SKErrorDomain {
+                    if error.domain == SKErrorDomain {
                         return alertWithTitle("Purchase failed", message: "Please check your Internet connection or try again later")
                     }
                     return alertWithTitle("Purchase failed", message: "Unknown error. Please contact support")
+                case .InvalidProductId(let productId):
+                    return alertWithTitle("Purchase failed", message: "\(productId) is not a valid product identifier")
                 case .NoProductIdentifier:
                     return alertWithTitle("Purchase failed", message: "Product not found")
                 case .PaymentNotAllowed:

+ 9 - 27
SwiftyStoreKit/InAppProductQueryRequest.swift

@@ -24,19 +24,9 @@
 
 import StoreKit
 
-public enum ResponseError : ErrorType {
-    case InvalidProducts(invalidProductIdentifiers: [String])
-    case NoProducts
-    case RequestFailed(error: NSError)
-}
 class InAppProductQueryRequest: NSObject, SKProductsRequestDelegate {
 
-    enum ResultType {
-        case Success(products: [SKProduct])
-        case Error(e: ResponseError)
-    }
-
-    typealias RequestCallback = (result: ResultType) -> ()
+    typealias RequestCallback = (result: SwiftyStoreKit.RetrieveResults) -> ()
     private let callback: RequestCallback
     private let request: SKProductsRequest
     // http://stackoverflow.com/questions/24011575/what-is-the-difference-between-a-weak-reference-and-an-unowned-reference
@@ -71,21 +61,13 @@ class InAppProductQueryRequest: NSObject, SKProductsRequestDelegate {
     // MARK: SKProductsRequestDelegate
     func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
         
-        if let invalidProductIdentifiers = response._invalidProductIdentifiers where invalidProductIdentifiers.count > 0 {
-            let error = ResponseError.InvalidProducts(invalidProductIdentifiers: invalidProductIdentifiers)
-            dispatch_async(dispatch_get_main_queue()) {
-                self.callback(result: .Error(e: error))
-            }
-            return
-        }
-        guard let products = response._products where products.count > 0 else {
-            let error = ResponseError.NoProducts
-            dispatch_async(dispatch_get_main_queue()) {
-                self.callback(result: .Error(e: error))
-            }
-            return
+        dispatch_async(dispatch_get_main_queue()) {
+            
+            let retrievedProducts = Set<SKProduct>(response.products ?? [])
+            let invalidProductIDs = Set<String>(response.invalidProductIdentifiers ?? [])
+            self.callback(result: SwiftyStoreKit.RetrieveResults(retrievedProducts: retrievedProducts,
+                invalidProductIDs: invalidProductIDs, error: nil))
         }
-        callback(result: .Success(products: products))
     }
     
     func requestDidFinish(request: SKRequest) {
@@ -107,9 +89,9 @@ class InAppProductQueryRequest: NSObject, SKProductsRequestDelegate {
     }
     #endif
     func requestFailed(error: NSError){
-        let error = ResponseError.RequestFailed(error: error)
         dispatch_async(dispatch_get_main_queue()) {
-            self.callback(result: .Error(e: error))
+            self.callback(result: SwiftyStoreKit.RetrieveResults(retrievedProducts: [],
+                invalidProductIDs: [], error: error))
         }
     }
 }

+ 36 - 43
SwiftyStoreKit/SwiftyStoreKit.swift

@@ -34,19 +34,36 @@ public class SwiftyStoreKit {
                 products[productIdentifier] = product
             }
         }
+        func allProductsMatching(productIds: Set<String>) -> Set<SKProduct>? {
+            var requestedProducts = Set<SKProduct>()
+            for productId in productIds {
+                guard let product = products[productId] else {
+                    return nil
+                }
+                requestedProducts.insert(product)
+            }
+            return requestedProducts
+        }
     }
     private var store: InAppPurchaseStore = InAppPurchaseStore()
 
     // As we can have multiple inflight queries and purchases, we store them in a dictionary by product id
-    private var inflightQueries: [String: InAppProductQueryRequest] = [:]
+    private var inflightQueries: [Set<String>: InAppProductQueryRequest] = [:]
     private var inflightPurchases: [String: InAppProductPurchaseRequest] = [:]
     private var restoreRequest: InAppProductPurchaseRequest?
     #if os(iOS)
     private var receiptRefreshRequest: InAppReceiptRefreshRequest?
     #endif
     // MARK: Enums
+    public struct RetrieveResults {
+        public let retrievedProducts: Set<SKProduct>
+        public let invalidProductIDs: Set<String>
+        public let error: NSError?
+    }
+
     public enum PurchaseError {
-        case Failed(error: ErrorType)
+        case Failed(error: NSError)
+        case InvalidProductId(productId: String)
         case NoProductIdentifier
         case PaymentNotAllowed
     }
@@ -54,10 +71,6 @@ public class SwiftyStoreKit {
         case Success(productId: String)
         case Error(error: PurchaseError)
     }
-    public enum RetrieveResult {
-        case Success(product: SKProduct)
-        case Error(error: ErrorType)
-    }
     public struct RestoreResults {
         public let restoredProductIds: [String]
         public let restoreFailedProducts: [(ErrorType, String?)]
@@ -79,21 +92,14 @@ public class SwiftyStoreKit {
     }
     
     // MARK: Public methods
-    public class func retrieveProductInfo(productId: String, completion: (result: RetrieveResult) -> ()) {
-        guard let product = sharedInstance.store.products[productId] else {
+    public class func retrieveProductsInfo(productIds: Set<String>, completion: (result: RetrieveResults) -> ()) {
+        
+        guard let products = sharedInstance.store.allProductsMatching(productIds) else {
             
-            sharedInstance.requestProduct(productId) { (inner: () throws -> SKProduct) -> () in
-                do {
-                    let product = try inner()
-                    completion(result: .Success(product: product))
-                }
-                catch let error {
-                    completion(result: .Error(error: error))
-                }
-            }
+            sharedInstance.requestProducts(productIds, completion: completion)
             return
         }
-        completion(result: .Success(product: product))
+        completion(result: RetrieveResults(retrievedProducts: products, invalidProductIDs: [], error: nil))
     }
     
     public class func purchaseProduct(productId: String, completion: (result: PurchaseResult) -> ()) {
@@ -102,20 +108,22 @@ public class SwiftyStoreKit {
             sharedInstance.purchase(product: product, completion: completion)
         }
         else {
-            retrieveProductInfo(productId) { (result) -> () in
-                if case .Success(let product) = result {
+            retrieveProductsInfo(Set([productId])) { result -> () in
+                if let product = result.retrievedProducts.first {
                     sharedInstance.purchase(product: product, completion: completion)
                 }
-                else if case .Error(let error) = result {
+                else if let error = result.error {
                     completion(result: .Error(error: .Failed(error: error)))
                 }
+                else if let invalidProductId = result.invalidProductIDs.first {
+                    completion(result: .Error(error: .InvalidProductId(productId: invalidProductId)))
+                }
             }
         }
     }
     
     public class func restorePurchases(completion: (results: RestoreResults) -> ()) {
 
-        // Called multiple
         sharedInstance.restoreRequest = InAppProductPurchaseRequest.restorePurchases() { results in
         
             sharedInstance.restoreRequest = nil
@@ -215,30 +223,15 @@ public class SwiftyStoreKit {
         return RestoreResults(restoredProductIds: restoredProductIds, restoreFailedProducts: restoreFailedProducts)
     }
     
-    
-    // http://appventure.me/2015/06/19/swift-try-catch-asynchronous-closures/
-    private func requestProduct(productId: String, completion: (result: (() throws -> SKProduct)) -> ()) -> () {
+    private func requestProducts(productIds: Set<String>, completion: (result: RetrieveResults) -> ()) {
         
-        inflightQueries[productId] = InAppProductQueryRequest.startQuery([productId]) { result in
+        inflightQueries[productIds] = InAppProductQueryRequest.startQuery(productIds) { result in
         
-            self.inflightQueries[productId] = nil
-            if case .Success(let products) = result {
-                
-                // Add to Store
-                for product in products {
-                    //print("Received product with ID: \(product.productIdentifier)")
-                    self.store.addProduct(product)
-                }
-                guard let product = self.store.products[productId] else {
-                    completion(result: { throw ResponseError.NoProducts })
-                    return
-                }
-                completion(result: { return product })
-            }
-            else if case .Error(let error) = result {
-                
-                completion(result: { throw error })
+            self.inflightQueries[productIds] = nil
+            for product in result.retrievedProducts {
+                self.store.addProduct(product)
             }
+            completion(result: result)
         }
     }
     

+ 13 - 10
SwiftyStoreOSXDemo/ViewController.swift

@@ -45,7 +45,7 @@ class ViewController: NSViewController {
     }
     func getInfo(no: String) {
 
-        SwiftyStoreKit.retrieveProductInfo(AppBundleId + ".purchase" + no) { result in
+        SwiftyStoreKit.retrieveProductsInfo([AppBundleId + ".purchase" + no]) { result in
 
             self.showAlert(self.alertForProductRetrievalInfo(result))
         }
@@ -104,14 +104,18 @@ extension ViewController {
         }
     }
 
-    func alertForProductRetrievalInfo(result: SwiftyStoreKit.RetrieveResult) -> NSAlert {
+    func alertForProductRetrievalInfo(result: SwiftyStoreKit.RetrieveResults) -> NSAlert {
         
-        switch result {
-        case .Success(let product):
+        if let product = result.retrievedProducts.first {
             let priceString = NSNumberFormatter.localizedStringFromNumber(product.price ?? 0, numberStyle: .CurrencyStyle)
             return alertWithTitle(product.localizedTitle ?? "no title", message: "\(product.localizedDescription) - \(priceString)")
-        case .Error(let error):
-            return alertWithTitle("Could not retrieve product info", message: String(error))
+        }
+        else if let invalidProductId = result.invalidProductIDs.first {
+            return alertWithTitle("Could not retrieve product info", message: "Invalid product identifier: \(invalidProductId)")
+        }
+        else {
+            let errorString = result.error?.localizedDescription ?? "Unknown error. Please contact support"
+            return alertWithTitle("Could not retrieve product info", message: errorString)
         }
     }
     
@@ -125,13 +129,12 @@ extension ViewController {
             print("Purchase Failed: \(error)")
             switch error {
             case .Failed(let error):
-                if case ResponseError.RequestFailed(let internalError) = error where internalError.domain == SKErrorDomain {
-                    return alertWithTitle("Purchase failed", message: "Please check your Internet connection or try again later")
-                }
-                if (error as NSError).domain == SKErrorDomain {
+                if error.domain == SKErrorDomain {
                     return alertWithTitle("Purchase failed", message: "Please check your Internet connection or try again later")
                 }
                 return alertWithTitle("Purchase failed", message: "Unknown error. Please contact support")
+            case .InvalidProductId(let productId):
+                return alertWithTitle("Purchase failed", message: "\(productId) is not a valid product identifier")
             case .NoProductIdentifier:
                 return alertWithTitle("Purchase failed", message: "Product not found")
             case .PaymentNotAllowed: