فهرست منبع

Implemented purchases in PaymentsController and started implementing RestorePurchasesController + tests

Andrea Bizzotto 8 سال پیش
والد
کامیت
aef356df01

+ 12 - 0
SwiftyStoreKit.xcodeproj/project.pbxproj

@@ -53,6 +53,9 @@
 		65F7DF9A1DCD536700835D30 /* SwiftyStoreKit-iOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 65F7DF971DCD536100835D30 /* SwiftyStoreKit-iOS.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		65F7DF9B1DCD537800835D30 /* SwiftyStoreKit-macOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 65F7DF981DCD536100835D30 /* SwiftyStoreKit-macOS.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		65F7DF9C1DCD537F00835D30 /* SwiftyStoreKit-tvOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 65F7DF991DCD536100835D30 /* SwiftyStoreKit-tvOS.h */; settings = {ATTRIBUTES = (Public, ); }; };
+		C3099C071E2FCDAA00392A54 /* PaymentsControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3099C061E2FCDAA00392A54 /* PaymentsControllerTests.swift */; };
+		C3099C091E2FCE3A00392A54 /* TestProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3099C081E2FCE3A00392A54 /* TestProduct.swift */; };
+		C3099C0B1E2FD13200392A54 /* TestPaymentTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3099C0A1E2FD13200392A54 /* TestPaymentTransaction.swift */; };
 		C4083C551C2AADB500295248 /* InAppReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A7C7621C29B8D00053ED64 /* InAppReceipt.swift */; };
 		C4083C571C2AB0A900295248 /* InAppReceiptRefreshRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4083C561C2AB0A900295248 /* InAppReceiptRefreshRequest.swift */; };
 		C40C68101C29414C00B60B7E /* OS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C680F1C29414C00B60B7E /* OS.swift */; };
@@ -158,6 +161,9 @@
 		65F7DF971DCD536100835D30 /* SwiftyStoreKit-iOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SwiftyStoreKit-iOS.h"; sourceTree = "<group>"; };
 		65F7DF981DCD536100835D30 /* SwiftyStoreKit-macOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SwiftyStoreKit-macOS.h"; sourceTree = "<group>"; };
 		65F7DF991DCD536100835D30 /* SwiftyStoreKit-tvOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SwiftyStoreKit-tvOS.h"; sourceTree = "<group>"; };
+		C3099C061E2FCDAA00392A54 /* PaymentsControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentsControllerTests.swift; sourceTree = "<group>"; };
+		C3099C081E2FCE3A00392A54 /* TestProduct.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestProduct.swift; sourceTree = "<group>"; };
+		C3099C0A1E2FD13200392A54 /* TestPaymentTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestPaymentTransaction.swift; sourceTree = "<group>"; };
 		C4083C561C2AB0A900295248 /* InAppReceiptRefreshRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppReceiptRefreshRequest.swift; sourceTree = "<group>"; };
 		C40C680F1C29414C00B60B7E /* OS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OS.swift; sourceTree = "<group>"; };
 		C4A7C7621C29B8D00053ED64 /* InAppReceipt.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppReceipt.swift; sourceTree = "<group>"; };
@@ -263,8 +269,11 @@
 			children = (
 				658A08421E2EC5120074A98F /* Info.plist */,
 				658A08491E2EC5350074A98F /* PaymentQueueControllerTests.swift */,
+				C3099C061E2FCDAA00392A54 /* PaymentsControllerTests.swift */,
 				658A084B1E2EC5960074A98F /* PaymentQueueSpy.swift */,
 				65F70AC61E2ECBB300BF040D /* PaymentTransactionObserverFake.swift */,
+				C3099C081E2FCE3A00392A54 /* TestProduct.swift */,
+				C3099C0A1E2FD13200392A54 /* TestPaymentTransaction.swift */,
 			);
 			path = SwiftyStoreKitTests;
 			sourceTree = "<group>";
@@ -611,9 +620,12 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				C3099C071E2FCDAA00392A54 /* PaymentsControllerTests.swift in Sources */,
+				C3099C0B1E2FD13200392A54 /* TestPaymentTransaction.swift in Sources */,
 				65F70AC71E2ECBB300BF040D /* PaymentTransactionObserverFake.swift in Sources */,
 				658A084A1E2EC5350074A98F /* PaymentQueueControllerTests.swift in Sources */,
 				658A084C1E2EC5960074A98F /* PaymentQueueSpy.swift in Sources */,
+				C3099C091E2FCE3A00392A54 /* TestProduct.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 10 - 0
SwiftyStoreKit.xcodeproj/xcshareddata/xcschemes/SwiftyStoreKit-iOS-Demo.xcscheme

@@ -28,6 +28,16 @@
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
       shouldUseLaunchSchemeArgsEnv = "YES">
       <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "658A083D1E2EC5120074A98F"
+               BuildableName = "SwiftyStoreKitTests.xctest"
+               BlueprintName = "SwiftyStoreKitTests"
+               ReferencedContainer = "container:SwiftyStoreKit.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
       </Testables>
       <MacroExpansion>
          <BuildableReference

+ 51 - 30
SwiftyStoreKit/PaymentQueueController.swift

@@ -40,30 +40,35 @@ public protocol PaymentQueue: class {
 extension SKPaymentQueue: PaymentQueue { }
 
 public class PaymentQueueController: NSObject, SKPaymentTransactionObserver {
-
-    public struct RestorePurchases {
-        let atomically: Bool
-        let callback: ([TransactionResult]) -> ()
-    }
     
     private let paymentsController: PaymentsController
     
+    private let restorePurchasesController: RestorePurchasesController
+    
     unowned let paymentQueue: PaymentQueue
 
     deinit {
         paymentQueue.remove(self)
     }
 
-    public init(paymentQueue: PaymentQueue = SKPaymentQueue.default(), paymentsController: PaymentsController = PaymentsController()) {
+    public init(paymentQueue: PaymentQueue = SKPaymentQueue.default(),
+                paymentsController: PaymentsController = PaymentsController(),
+                restorePurchasesController: RestorePurchasesController = RestorePurchasesController()) {
      
         self.paymentQueue = paymentQueue
         self.paymentsController = paymentsController
+        self.restorePurchasesController = restorePurchasesController
         super.init()
         paymentQueue.add(self)
     }
     
     public func startPayment(_ payment: Payment) {
         
+        if paymentsController.hasPayment(payment) {
+            // return .inProgress
+            return
+        }
+        
         let skPayment = SKMutablePayment(product: payment.product)
         skPayment.applicationUsername = payment.applicationUsername
         paymentQueue.add(skPayment)
@@ -71,34 +76,50 @@ public class PaymentQueueController: NSObject, SKPaymentTransactionObserver {
         paymentsController.insert(payment)
     }
     
+    public func startRestorePurchases(_ restorePurchases: RestorePurchases) {
+        
+        if restorePurchasesController.restorePurchases != nil {
+            // return .inProgress
+            return
+        }
+        
+        paymentQueue.restoreCompletedTransactions()
+        
+        restorePurchasesController.restorePurchases = restorePurchases
+    }
+    
     
     // MARK: SKPaymentTransactionObserver
     public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
+        
+        var unhandledTransactions = paymentsController.processTransactions(transactions, on: paymentQueue)
+        
+        unhandledTransactions = restorePurchasesController.processTransactions(unhandledTransactions, on: paymentQueue)
 
-        for transaction in transactions {
-            
-            let transactionState = transaction.transactionState
-
-            switch transactionState {
-            case .purchased:
-                
-                let _ = paymentsController.processTransaction(transaction, paymentQueue: paymentQueue)
-                
-                break
-            case .failed:
-                break
-
-            case .restored:
-                break
-
-            case .purchasing:
-                // In progress: do nothing
-                break
-            case .deferred:
-                break
-            }
-
-        }
+        // TODO: Complete transactions
+        
+//        for transaction in transactionsExcludingPayments {
+//            
+//            let transactionState = transaction.transactionState
+//
+//            switch transactionState {
+//            case .purchased:
+//                
+//                break
+//            case .failed:
+//                break
+//
+//            case .restored:
+//                break
+//
+//            case .purchasing:
+//                // In progress: do nothing
+//                break
+//            case .deferred:
+//                break
+//            }
+//
+//        }
         
     }
     

+ 64 - 9
SwiftyStoreKit/Payments.swift

@@ -9,12 +9,28 @@
 import Foundation
 import StoreKit
 
+
+public protocol TransactionController {
+    
+    /**
+     * - param transactions: transactions to process
+     * - param paymentQueue: payment queue for finishing transactions
+     * - return: array of unhandled transactions
+     */
+    func processTransactions(_ transactions: [SKPaymentTransaction], on paymentQueue: PaymentQueue) -> [SKPaymentTransaction]
+}
+
 public enum TransactionResult {
     case purchased(product: Product)
     case restored(product: Product)
     case failed(error: Error)
 }
 
+public struct RestorePurchases {
+    let atomically: Bool
+    let callback: ([TransactionResult]) -> ()
+}
+
 public struct Payment: Hashable {
     public let product: SKProduct
     public let atomically: Bool
@@ -29,10 +45,12 @@ public struct Payment: Hashable {
     }
 }
 
-public class PaymentsController {
+public class PaymentsController: TransactionController {
     
     private var payments: Set<Payment> = []
     
+    public init() { }
+    
     private func findPayment(withProductIdentifier identifier: String) -> Payment? {
         for payment in payments {
             if payment.product.productIdentifier == identifier {
@@ -42,26 +60,63 @@ public class PaymentsController {
         return nil
     }
     
+    public func hasPayment(_ payment: Payment) -> Bool {
+        return findPayment(withProductIdentifier: payment.product.productIdentifier) != nil
+    }
+    
     public func insert(_ payment: Payment) {
         payments.insert(payment)
     }
     
-    public func processTransaction(_ transaction: SKPaymentTransaction, paymentQueue: PaymentQueue) -> Bool {
+    public func processTransaction(_ transaction: SKPaymentTransaction, on paymentQueue: PaymentQueue) -> Bool {
         
         let transactionProductIdentifier = transaction.payment.productIdentifier
         
         if let payment = findPayment(withProductIdentifier: transactionProductIdentifier) {
+
+            let transactionState = transaction.transactionState
             
-            let product = Product(productId: transactionProductIdentifier, transaction: transaction, needsFinishTransaction: !payment.atomically)
-            
-            payment.callback(.purchased(product: product))
-            
-            if payment.atomically {
+            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 {
+
+                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))
+                
                 paymentQueue.finishTransaction(transaction)
+                payments.remove(payment)
+                return true
+            }
+            
+            if transactionState == .restored {
+                print("Unexpected restored transaction for payment \(transactionProductIdentifier)")
             }
-            payments.remove(payment)
-            return true
         }
         return false
     }
+    
+    public func processTransactions(_ transactions: [SKPaymentTransaction], on paymentQueue: PaymentQueue) -> [SKPaymentTransaction] {
+        
+        return transactions.filter { !processTransaction($0, on: paymentQueue) }
+    }
+}
+
+public class RestorePurchasesController: TransactionController {
+
+    public var restorePurchases: RestorePurchases?
+    
+    public func processTransactions(_ transactions: [SKPaymentTransaction], on paymentQueue: PaymentQueue) -> [SKPaymentTransaction] {
+        return []
+    }
 }

+ 0 - 14
SwiftyStoreKitTests/PaymentQueueControllerTests.swift

@@ -37,20 +37,6 @@ extension Payment {
 
 class PaymentQueueControllerTests: XCTestCase {
 
-    class TestProduct: SKProduct {
-        
-        var _productIdentifier: String = ""
-        
-        override var productIdentifier: String {
-            return _productIdentifier
-        }
-        
-        init(productIdentifier: String) {
-            _productIdentifier = productIdentifier
-            super.init()
-        }
-    }
-    
     // MARK: init/deinit
     func testInit_registersAsObserver() {
         

+ 3 - 0
SwiftyStoreKitTests/PaymentQueueSpy.swift

@@ -16,6 +16,8 @@ class PaymentQueueSpy: PaymentQueue {
     var payments: [SKPayment] = []
     
     var restoreCompletedTransactionCalledCount = 0
+    
+    var finishTransactionCalledCount = 0
 
     func add(_ observer: SKPaymentTransactionObserver) {
         
@@ -40,5 +42,6 @@ class PaymentQueueSpy: PaymentQueue {
 
     func finishTransaction(_ transaction: SKPaymentTransaction) {
         
+        finishTransactionCalledCount += 1
     }
 }

+ 98 - 0
SwiftyStoreKitTests/PaymentsControllerTests.swift

@@ -0,0 +1,98 @@
+//
+// PaymentsControllerTests.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 PaymentsControllerTests: XCTestCase {
+
+    func testInsertPayment_hasPayment() {
+     
+        let payment = makeTestPayment(productIdentifier: "com.SwiftyStoreKit.product1") { result in }
+
+        let paymentsController = makePaymentsController(insertPayment: payment)
+        
+        XCTAssertTrue(paymentsController.hasPayment(payment))
+    }
+    
+    func testProcessTransaction_when_transactionStatePurchased_then_removesPayment_finishesTransaction_callsCallback() {
+        
+        let productIdentifier = "com.SwiftyStoreKit.product1"
+        let product = TestProduct(productIdentifier: productIdentifier)
+        
+        var callbackCalled = false
+        let payment = makeTestPayment(product: product) { result in
+            
+            callbackCalled = true
+            if case .purchased(let product) = result {
+                XCTAssertEqual(product.productId, productIdentifier)
+            }
+            else {
+                XCTFail("expected purchased callback with product id")
+            }
+        }
+        
+        let paymentsController = makePaymentsController(insertPayment: payment)
+        
+        let transaction = TestPaymentTransaction(payment: SKPayment(product: product), transactionState: .purchased)
+        
+        let spy = PaymentQueueSpy()
+        
+        let remainingTransactions = paymentsController.processTransactions([transaction], on: spy)
+        
+        XCTAssertEqual(remainingTransactions.count, 0)
+
+        XCTAssertFalse(paymentsController.hasPayment(payment))
+        
+        XCTAssertTrue(callbackCalled)
+        
+        XCTAssertEqual(spy.finishTransactionCalledCount, 1)
+    }
+    
+    func testProcessTransaction_when_transactionStateFailed_then_removesPayment_finishesTransaction_callsCallback() {
+        
+    }
+    
+    func makePaymentsController(insertPayment payment: Payment) -> PaymentsController {
+        
+        let paymentsController = PaymentsController()
+        
+        paymentsController.insert(payment)
+        
+        return paymentsController
+    }
+    
+    func makeTestPayment(product: SKProduct, atomically: Bool = true, callback: @escaping (TransactionResult) -> ()) -> Payment {
+        
+        return Payment(product: product, atomically: atomically, applicationUsername: "", callback: callback)
+    }
+    
+    func makeTestPayment(productIdentifier: String, atomically: Bool = true, callback: @escaping (TransactionResult) -> ()) -> Payment {
+
+        let product = TestProduct(productIdentifier: productIdentifier)
+        return makeTestPayment(product: product, atomically: atomically, callback: callback)
+
+    }
+}

+ 44 - 0
SwiftyStoreKitTests/TestPaymentTransaction.swift

@@ -0,0 +1,44 @@
+//
+// TestPaymentTransaction.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 StoreKit
+
+class TestPaymentTransaction: SKPaymentTransaction {
+
+    let _transactionState: SKPaymentTransactionState
+    let _payment: SKPayment
+    
+    init(payment: SKPayment, transactionState: SKPaymentTransactionState) {
+        _transactionState = transactionState
+        _payment = payment
+    }
+
+    override var payment: SKPayment {
+        return _payment
+    }
+    
+    override var transactionState: SKPaymentTransactionState {
+        return _transactionState
+    }
+}

+ 39 - 0
SwiftyStoreKitTests/TestProduct.swift

@@ -0,0 +1,39 @@
+//
+// TestProduct.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 StoreKit
+
+class TestProduct: SKProduct {
+    
+    var _productIdentifier: String = ""
+    
+    override var productIdentifier: String {
+        return _productIdentifier
+    }
+    
+    init(productIdentifier: String) {
+        _productIdentifier = productIdentifier
+        super.init()
+    }
+}