Эх сурвалжийг харах

Implemented RestorePurchasesController and tests

Andrea Bizzotto 8 жил өмнө
parent
commit
2adfadf856

+ 7 - 2
SwiftyStoreKit.xcodeproj/project.pbxproj

@@ -20,6 +20,7 @@
 		6502F63A1B985C9E004E342D /* InAppProductPurchaseRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6502F6221B98586A004E342D /* InAppProductPurchaseRequest.swift */; };
 		6502F63B1B985CA1004E342D /* InAppProductQueryRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6502F6231B98586A004E342D /* InAppProductQueryRequest.swift */; };
 		6502F63C1B985CA4004E342D /* SwiftyStoreKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6502F6241B98586A004E342D /* SwiftyStoreKit.swift */; };
+		650307F21E3163AA001332A4 /* RestorePurchasesControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 650307F11E3163AA001332A4 /* RestorePurchasesControllerTests.swift */; };
 		651A71251CD651AF000B4091 /* InAppCompleteTransactionsObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 651A71241CD651AF000B4091 /* InAppCompleteTransactionsObserver.swift */; };
 		651A71261CD651AF000B4091 /* InAppCompleteTransactionsObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 651A71241CD651AF000B4091 /* InAppCompleteTransactionsObserver.swift */; };
 		653722811DB8282600C8F944 /* SKProduct+LocalizedPrice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653722801DB8282600C8F944 /* SKProduct+LocalizedPrice.swift */; };
@@ -133,6 +134,7 @@
 		6502F6231B98586A004E342D /* InAppProductQueryRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppProductQueryRequest.swift; sourceTree = "<group>"; };
 		6502F6241B98586A004E342D /* SwiftyStoreKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyStoreKit.swift; sourceTree = "<group>"; };
 		6502F62D1B985C40004E342D /* SwiftyStoreKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftyStoreKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		650307F11E3163AA001332A4 /* RestorePurchasesControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestorePurchasesControllerTests.swift; sourceTree = "<group>"; };
 		651A71241CD651AF000B4091 /* InAppCompleteTransactionsObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppCompleteTransactionsObserver.swift; sourceTree = "<group>"; };
 		653722801DB8282600C8F944 /* SKProduct+LocalizedPrice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SKProduct+LocalizedPrice.swift"; sourceTree = "<group>"; };
 		658A08361E2EC24E0074A98F /* PaymentQueueController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentQueueController.swift; sourceTree = "<group>"; };
@@ -270,6 +272,7 @@
 				658A08421E2EC5120074A98F /* Info.plist */,
 				658A08491E2EC5350074A98F /* PaymentQueueControllerTests.swift */,
 				C3099C061E2FCDAA00392A54 /* PaymentsControllerTests.swift */,
+				650307F11E3163AA001332A4 /* RestorePurchasesControllerTests.swift */,
 				658A084B1E2EC5960074A98F /* PaymentQueueSpy.swift */,
 				65F70AC61E2ECBB300BF040D /* PaymentTransactionObserverFake.swift */,
 				C3099C081E2FCE3A00392A54 /* TestProduct.swift */,
@@ -473,6 +476,7 @@
 					};
 					6502F5FD1B985833004E342D = {
 						CreatedOnToolsVersion = 7.0;
+						DevelopmentTeam = M54ZVB688G;
 						LastSwiftMigration = 0800;
 						ProvisioningStyle = Automatic;
 					};
@@ -621,6 +625,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				C3099C071E2FCDAA00392A54 /* PaymentsControllerTests.swift in Sources */,
+				650307F21E3163AA001332A4 /* RestorePurchasesControllerTests.swift in Sources */,
 				C3099C0B1E2FD13200392A54 /* TestPaymentTransaction.swift in Sources */,
 				65F70AC71E2ECBB300BF040D /* PaymentTransactionObserverFake.swift in Sources */,
 				658A084A1E2EC5350074A98F /* PaymentQueueControllerTests.swift in Sources */,
@@ -854,7 +859,7 @@
 				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
-				DEVELOPMENT_TEAM = "";
+				DEVELOPMENT_TEAM = M54ZVB688G;
 				INFOPLIST_FILE = "$(SRCROOT)/SwiftyStoreKit-iOS-Demo/Info.plist";
 				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@@ -870,7 +875,7 @@
 				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
-				DEVELOPMENT_TEAM = "";
+				DEVELOPMENT_TEAM = M54ZVB688G;
 				INFOPLIST_FILE = "$(SRCROOT)/SwiftyStoreKit-iOS-Demo/Info.plist";
 				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";

+ 67 - 29
SwiftyStoreKit/Payments.swift

@@ -27,8 +27,13 @@ public enum TransactionResult {
 }
 
 public struct RestorePurchases {
-    let atomically: Bool
-    let callback: ([TransactionResult]) -> ()
+    public let atomically: Bool
+    public let callback: ([TransactionResult]) -> ()
+    
+    public init(atomically: Bool, callback: @escaping ([TransactionResult]) -> ()) {
+        self.atomically = atomically
+        self.callback = callback
+    }
 }
 
 public struct Payment: Hashable {
@@ -72,36 +77,37 @@ public class PaymentsController: TransactionController {
         
         let transactionProductIdentifier = transaction.payment.productIdentifier
         
-        if let payment = findPayment(withProductIdentifier: transactionProductIdentifier) {
+        guard let payment = findPayment(withProductIdentifier: transactionProductIdentifier) else {
 
-            let transactionState = transaction.transactionState
-            
-            if transactionState == .purchased {
-
-                let product = Product(productId: transactionProductIdentifier, transaction: transaction, needsFinishTransaction: !payment.atomically)
-                
-                payment.callback(.purchased(product: product))
-                
-                if payment.atomically {
-                    paymentQueue.finishTransaction(transaction)
-                }
-                payments.remove(payment)
-                return true
-            }
-            if transactionState == .failed {
+            return false
+        }
+        let transactionState = transaction.transactionState
+        
+        if transactionState == .purchased {
 
-                let message = "Transaction failed for product ID: \(transactionProductIdentifier)"
-                let altError = NSError(domain: SKErrorDomain, code: 0, userInfo: [ NSLocalizedDescriptionKey: message ])
-                payment.callback(.failed(error: transaction.error ?? altError))
-                
+            let product = Product(productId: transactionProductIdentifier, transaction: transaction, needsFinishTransaction: !payment.atomically)
+            
+            payment.callback(.purchased(product: product))
+            
+            if payment.atomically {
                 paymentQueue.finishTransaction(transaction)
-                payments.remove(payment)
-                return true
             }
+            payments.remove(payment)
+            return true
+        }
+        if transactionState == .failed {
+
+            let message = "Transaction failed for product ID: \(transactionProductIdentifier)"
+            let altError = NSError(domain: SKErrorDomain, code: 0, userInfo: [ NSLocalizedDescriptionKey: message ])
+            payment.callback(.failed(error: transaction.error ?? altError))
             
-            if transactionState == .restored {
-                print("Unexpected restored transaction for payment \(transactionProductIdentifier)")
-            }
+            paymentQueue.finishTransaction(transaction)
+            payments.remove(payment)
+            return true
+        }
+        
+        if transactionState == .restored {
+            print("Unexpected restored transaction for payment \(transactionProductIdentifier)")
         }
         return false
     }
@@ -116,13 +122,45 @@ public class RestorePurchasesController: TransactionController {
 
     public var restorePurchases: RestorePurchases?
     
+    public init() { }
+    
+    public func processTransaction(_ transaction: SKPaymentTransaction, atomically: Bool, on paymentQueue: PaymentQueue) -> Product? {
+        
+        let transactionState = transaction.transactionState
+
+        if transactionState == .restored {
+
+            let transactionProductIdentifier = transaction.payment.productIdentifier
+
+            let product = Product(productId: transactionProductIdentifier, transaction: transaction, needsFinishTransaction: !atomically)
+            if atomically {
+                paymentQueue.finishTransaction(transaction)
+            }
+            return product
+        }
+        return nil
+    }
+    
     public func processTransactions(_ transactions: [SKPaymentTransaction], on paymentQueue: PaymentQueue) -> [SKPaymentTransaction] {
         
         guard let restorePurchases = restorePurchases else {
             return transactions
         }
-        // TODO: process
-        return []
+        
+        var unhandledTransactions: [SKPaymentTransaction] = []
+        var restoredProducts: [TransactionResult] = []
+        for transaction in transactions {
+            if let restoredProduct = processTransaction(transaction, atomically: restorePurchases.atomically, on: paymentQueue) {
+                restoredProducts.append(.restored(product: restoredProduct))
+            }
+            else {
+                unhandledTransactions.append(transaction)
+            }
+        }
+        if restoredProducts.count > 0 {
+            restorePurchases.callback(restoredProducts)
+        }
+        return unhandledTransactions
     }
     
     public func restoreCompletedTransactionsFailed(withError error: Error) {

+ 6 - 6
SwiftyStoreKitTests/PaymentsControllerTests.swift

@@ -40,10 +40,10 @@ class PaymentsControllerTests: XCTestCase {
     func testProcessTransaction_when_transactionStatePurchased_then_removesPayment_finishesTransaction_callsCallback() {
         
         let productIdentifier = "com.SwiftyStoreKit.product1"
-        let product = TestProduct(productIdentifier: productIdentifier)
+        let testProduct = TestProduct(productIdentifier: productIdentifier)
         
         var callbackCalled = false
-        let payment = makeTestPayment(product: product) { result in
+        let payment = makeTestPayment(product: testProduct) { result in
             
             callbackCalled = true
             if case .purchased(let product) = result {
@@ -56,7 +56,7 @@ class PaymentsControllerTests: XCTestCase {
         
         let paymentsController = makePaymentsController(insertPayment: payment)
         
-        let transaction = TestPaymentTransaction(payment: SKPayment(product: product), transactionState: .purchased)
+        let transaction = TestPaymentTransaction(payment: SKPayment(product: testProduct), transactionState: .purchased)
         
         let spy = PaymentQueueSpy()
         
@@ -74,10 +74,10 @@ class PaymentsControllerTests: XCTestCase {
     func testProcessTransaction_when_transactionStateFailed_then_removesPayment_finishesTransaction_callsCallback() {
         
         let productIdentifier = "com.SwiftyStoreKit.product1"
-        let product = TestProduct(productIdentifier: productIdentifier)
+        let testProduct = TestProduct(productIdentifier: productIdentifier)
         
         var callbackCalled = false
-        let payment = makeTestPayment(product: product) { result in
+        let payment = makeTestPayment(product: testProduct) { result in
             
             callbackCalled = true
             if case .failed(_) = result {
@@ -90,7 +90,7 @@ class PaymentsControllerTests: XCTestCase {
         
         let paymentsController = makePaymentsController(insertPayment: payment)
         
-        let transaction = TestPaymentTransaction(payment: SKPayment(product: product), transactionState: .failed)
+        let transaction = TestPaymentTransaction(payment: SKPayment(product: testProduct), transactionState: .failed)
         
         let spy = PaymentQueueSpy()
         

+ 122 - 0
SwiftyStoreKitTests/RestorePurchasesControllerTests.swift

@@ -0,0 +1,122 @@
+//
+// RestorePurchasesControllerTests.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
+import SwiftyStoreKit
+import StoreKit
+
+class RestorePurchasesControllerTests: XCTestCase {
+    
+    
+    func testProcessTransactions_when_oneRestoredTransaction_then_finishesTransaction_callsCallback_noRemainingTransactions() {
+        
+        let productIdentifier = "com.SwiftyStoreKit.product1"
+        let testProduct = TestProduct(productIdentifier: productIdentifier)
+        
+        let transaction = TestPaymentTransaction(payment: SKPayment(product: testProduct), transactionState: .restored)
+
+        var callbackCalled = false
+        let restorePurchases = RestorePurchases(atomically: true) { results in
+            callbackCalled = true
+            XCTAssertEqual(results.count, 1)
+            let restored = results.first!
+            if case .restored(let restoredProduct) = restored {
+                XCTAssertEqual(restoredProduct.productId, productIdentifier)
+            }
+            else {
+                XCTFail("expected restored callback with product")
+            }
+        }
+        
+        let restorePurchasesController = makeRestorePurchasesController(restorePurchases: restorePurchases)
+        
+        let spy = PaymentQueueSpy()
+
+        let remainingTransactions = restorePurchasesController.processTransactions([transaction], on: spy)
+        
+        XCTAssertEqual(remainingTransactions.count, 0)
+
+        XCTAssertTrue(callbackCalled)
+
+        XCTAssertEqual(spy.finishTransactionCalledCount, 1)
+    }
+
+    func testProcessTransactions_when_twoRestoredTransactions_oneFailedTransaction_onePurchasedTransaction_then_finishesTwoTransactions_callsCallback_twoRemainingTransaction() {
+        
+        let productIdentifier1 = "com.SwiftyStoreKit.product1"
+        let testProduct1 = TestProduct(productIdentifier: productIdentifier1)
+        let transaction1 = TestPaymentTransaction(payment: SKPayment(product: testProduct1), transactionState: .restored)
+
+        let productIdentifier2 = "com.SwiftyStoreKit.product2"
+        let testProduct2 = TestProduct(productIdentifier: productIdentifier2)
+        let transaction2 = TestPaymentTransaction(payment: SKPayment(product: testProduct2), transactionState: .restored)
+
+        let productIdentifier3 = "com.SwiftyStoreKit.product3"
+        let testProduct3 = TestProduct(productIdentifier: productIdentifier3)
+        let transaction3 = TestPaymentTransaction(payment: SKPayment(product: testProduct3), transactionState: .failed)
+
+        let productIdentifier4 = "com.SwiftyStoreKit.product4"
+        let testProduct4 = TestProduct(productIdentifier: productIdentifier4)
+        let transaction4 = TestPaymentTransaction(payment: SKPayment(product: testProduct4), transactionState: .purchased)
+
+        let transactions = [transaction1, transaction2, transaction3, transaction4]
+
+        var callbackCalled = false
+        let restorePurchases = RestorePurchases(atomically: true) { results in
+            callbackCalled = true
+            XCTAssertEqual(results.count, 2)
+            let first = results.first!
+            let last = results.last!
+            if case .restored(let restoredProduct) = restored {
+                XCTAssertEqual(restoredProduct.productId, productIdentifier1)
+            }
+            else {
+                XCTFail("expected restored callback with product")
+            }
+        }
+        
+        let restorePurchasesController = makeRestorePurchasesController(restorePurchases: restorePurchases)
+        
+        let spy = PaymentQueueSpy()
+        
+        let remainingTransactions = restorePurchasesController.processTransactions(transactions, on: spy)
+        
+        XCTAssertEqual(remainingTransactions.count, 2)
+        
+        XCTAssertTrue(callbackCalled)
+        
+        XCTAssertEqual(spy.finishTransactionCalledCount, 1)
+    }
+
+    
+    func makeRestorePurchasesController(restorePurchases: RestorePurchases?) -> RestorePurchasesController {
+        
+        let restorePurchasesController = RestorePurchasesController()
+        
+        restorePurchasesController.restorePurchases = restorePurchases
+        
+        return restorePurchasesController
+    }
+}