From 2833a4f77f6146039646cb17b49b9aba9b149d91 Mon Sep 17 00:00:00 2001
From: Luong Huy Duc <luonghuyduc88@gmail.com>
Date: Thu, 12 Jan 2017 16:59:02 +0800
Subject: [PATCH 1/4] Add ReceiptValidator protocol

Conform to this protocol to provide custom receipt validator
---
 SwiftyStoreKit/SwiftyStoreKit+Types.swift | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/SwiftyStoreKit/SwiftyStoreKit+Types.swift b/SwiftyStoreKit/SwiftyStoreKit+Types.swift
index 3cdf15a4..76785cc5 100644
--- a/SwiftyStoreKit/SwiftyStoreKit+Types.swift
+++ b/SwiftyStoreKit/SwiftyStoreKit+Types.swift
@@ -33,6 +33,11 @@ public struct Product {
     public let needsFinishTransaction: Bool
 }
 
+//Conform to this protocol to provide custom receipt validator
+public protocol ReceiptValidator {
+	func validate(receipt: String, password autoRenewPassword: String?, completion: @escaping (VerifyReceiptResult) -> Void)
+}
+
 // Payment transaction
 public protocol PaymentTransaction {
     var transactionState: SKPaymentTransactionState { get }

From 70264da88130f726c74924d9a72487aba27a8c96 Mon Sep 17 00:00:00 2001
From: Luong Huy Duc <luonghuyduc88@gmail.com>
Date: Thu, 12 Jan 2017 17:00:39 +0800
Subject: [PATCH 2/4] Extract validation code to AppleReceiptValidator which
 conforms to ReceiptValidator protocol

---
 SwiftyStoreKit/InAppReceipt.swift      |  81 +----------------
 SwiftyStoreKit/ReceiptValidators.swift | 120 +++++++++++++++++++++++++
 2 files changed, 121 insertions(+), 80 deletions(-)
 create mode 100644 SwiftyStoreKit/ReceiptValidators.swift

diff --git a/SwiftyStoreKit/InAppReceipt.swift b/SwiftyStoreKit/InAppReceipt.swift
index 3395b3e8..95b5c9fb 100644
--- a/SwiftyStoreKit/InAppReceipt.swift
+++ b/SwiftyStoreKit/InAppReceipt.swift
@@ -28,12 +28,6 @@ import Foundation
 // MARK - receipt mangement
 internal class InAppReceipt {
 
-    // URL used to verify remotely receipt
-    enum VerifyReceiptURLType: String {
-        case production = "https://buy.itunes.apple.com/verifyReceipt"
-        case sandbox = "https://sandbox.itunes.apple.com/verifyReceipt"
-    }
-
     static var appStoreReceiptUrl: URL? {
         return Bundle.main.appStoreReceiptURL
     }
@@ -70,80 +64,7 @@ internal class InAppReceipt {
                 return
             }
 
-            // Create request
-            let storeURL = URL(string: urlType.rawValue)! // safe (until no more)
-            let storeRequest = NSMutableURLRequest(url: storeURL)
-            storeRequest.httpMethod = "POST"
-
-
-            let requestContents: NSMutableDictionary = [ "receipt-data" : base64EncodedString ]
-            // password if defined
-            if let password = autoRenewPassword {
-                requestContents.setValue(password, forKey: "password")
-            }
-
-            // Encore request body
-            do {
-                storeRequest.httpBody = try JSONSerialization.data(withJSONObject: requestContents, options: [])
-            } catch let e {
-                completion(.error(error: .requestBodyEncodeError(error: e)))
-                return
-            }
-
-            // Remote task
-            let task = session.dataTask(with: storeRequest as URLRequest) { data, response, error -> Void in
-
-                // there is an error
-                if let networkError = error {
-                    completion(.error(error: .networkError(error: networkError)))
-                    return
-                }
-
-                // there is no data
-                guard let safeData = data else {
-                    completion(.error(error: .noRemoteData))
-                    return
-                }
-
-                // cannot decode data
-                guard let receiptInfo = try? JSONSerialization.jsonObject(with: data!, options: .mutableLeaves) as? ReceiptInfo ?? [:] else {
-                    let jsonStr = String(data: safeData, encoding: String.Encoding.utf8)
-                    completion(.error(error: .jsonDecodeError(string: jsonStr)))
-                    return
-                }
-
-                // get status from info
-                if let status = receiptInfo["status"] as? Int {
-                    /*
-                     * http://stackoverflow.com/questions/16187231/how-do-i-know-if-an-in-app-purchase-receipt-comes-from-the-sandbox
-                     * How do I verify my receipt (iOS)?
-                     * Always verify your receipt first with the production URL; proceed to verify
-                     * with the sandbox URL if you receive a 21007 status code. Following this
-                     * approach ensures that you do not have to switch between URLs while your
-                     * application is being tested or reviewed in the sandbox or is live in the
-                     * App Store.
-
-                     * Note: The 21007 status code indicates that this receipt is a sandbox receipt,
-                     * but it was sent to the production service for verification.
-                     */
-                    let receiptStatus = ReceiptStatus(rawValue: status) ?? ReceiptStatus.unknown
-                    if case .testReceipt = receiptStatus {
-                        verify(urlType: .sandbox, password: autoRenewPassword, session: session, completion: completion)
-                    }
-                    else {
-                        if receiptStatus.isValid {
-                            completion(.success(receipt: receiptInfo))
-                        }
-                        else {
-                            completion(.error(error: .receiptInvalid(receipt: receiptInfo, status: receiptStatus)))
-                        }
-                    }
-                }
-                else {
-                    completion(.error(error: .receiptInvalid(receipt: receiptInfo, status: ReceiptStatus.none)))
-                }
-            }
-            task.resume()
+			validator.validate(receipt: base64EncodedString, password: autoRenewPassword, completion: completion)
     }
   
     /**
diff --git a/SwiftyStoreKit/ReceiptValidators.swift b/SwiftyStoreKit/ReceiptValidators.swift
new file mode 100644
index 00000000..8d7ed81d
--- /dev/null
+++ b/SwiftyStoreKit/ReceiptValidators.swift
@@ -0,0 +1,120 @@
+//
+//  InAppReceipt.swift
+//  SwiftyStoreKit
+//
+//  Created by phimage on 22/12/15.
+// Copyright (c) 2015 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 Foundation
+
+public struct AppleReceiptValidator: ReceiptValidator {
+
+	public enum VerifyReceiptURLType: String {
+		case production = "https://buy.itunes.apple.com/verifyReceipt"
+		case sandbox = "https://sandbox.itunes.apple.com/verifyReceipt"
+	}
+
+	public init(service: VerifyReceiptURLType) {
+		self.service = service
+	}
+
+	private let service: VerifyReceiptURLType
+
+	public func validate(
+		receipt: String,
+		password autoRenewPassword: String? = nil,
+		completion: @escaping (VerifyReceiptResult) -> Void) {
+
+		let storeURL = URL(string: service.rawValue)! // safe (until no more)
+		let storeRequest = NSMutableURLRequest(url: storeURL)
+		storeRequest.httpMethod = "POST"
+
+		let requestContents: NSMutableDictionary = [ "receipt-data" : receipt ]
+		// password if defined
+		if let password = autoRenewPassword {
+			requestContents.setValue(password, forKey: "password")
+		}
+
+		// Encore request body
+		do {
+			storeRequest.httpBody = try JSONSerialization.data(withJSONObject: requestContents, options: [])
+		} catch let e {
+			completion(.error(error: .requestBodyEncodeError(error: e)))
+			return
+		}
+
+		// Remote task
+		let task = URLSession.shared.dataTask(with: storeRequest as URLRequest) { data, response, error -> Void in
+
+			// there is an error
+			if let networkError = error {
+				completion(.error(error: .networkError(error: networkError)))
+				return
+			}
+
+			// there is no data
+			guard let safeData = data else {
+				completion(.error(error: .noRemoteData))
+				return
+			}
+
+			// cannot decode data
+			guard let receiptInfo = try? JSONSerialization.jsonObject(with: data!, options: .mutableLeaves) as? ReceiptInfo ?? [:] else {
+				let jsonStr = String(data: safeData, encoding: String.Encoding.utf8)
+				completion(.error(error: .jsonDecodeError(string: jsonStr)))
+				return
+			}
+
+			// get status from info
+			if let status = receiptInfo["status"] as? Int {
+				/*
+				* http://stackoverflow.com/questions/16187231/how-do-i-know-if-an-in-app-purchase-receipt-comes-from-the-sandbox
+				* How do I verify my receipt (iOS)?
+				* Always verify your receipt first with the production URL; proceed to verify
+				* with the sandbox URL if you receive a 21007 status code. Following this
+				* approach ensures that you do not have to switch between URLs while your
+				* application is being tested or reviewed in the sandbox or is live in the
+				* App Store.
+
+				* Note: The 21007 status code indicates that this receipt is a sandbox receipt,
+				* but it was sent to the production service for verification.
+				*/
+				let receiptStatus = ReceiptStatus(rawValue: status) ?? ReceiptStatus.unknown
+				if case .testReceipt = receiptStatus {
+					let sandboxValidator = AppleReceiptValidator(service: .sandbox)
+					sandboxValidator.validate(receipt: receipt, password: autoRenewPassword, completion: completion)
+				}
+				else {
+					if receiptStatus.isValid {
+						completion(.success(receipt: receiptInfo))
+					}
+					else {
+						completion(.error(error: .receiptInvalid(receipt: receiptInfo, status: receiptStatus)))
+					}
+				}
+			}
+			else {
+				completion(.error(error: .receiptInvalid(receipt: receiptInfo, status: ReceiptStatus.none)))
+			}
+		}
+		task.resume()
+	}
+}

From e801d51a2394e4c2da117990f45c9921e2bfc5c8 Mon Sep 17 00:00:00 2001
From: Luong Huy Duc <luonghuyduc88@gmail.com>
Date: Thu, 12 Jan 2017 17:02:20 +0800
Subject: [PATCH 3/4] Change method signature to inject ReceiptValidator in

---
 SwiftyStoreKit.xcodeproj/project.pbxproj | 8 ++++++++
 SwiftyStoreKit/InAppReceipt.swift        | 3 +--
 SwiftyStoreKit/SwiftyStoreKit.swift      | 5 +++--
 3 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/SwiftyStoreKit.xcodeproj/project.pbxproj b/SwiftyStoreKit.xcodeproj/project.pbxproj
index bf30e1f4..267a34d4 100644
--- a/SwiftyStoreKit.xcodeproj/project.pbxproj
+++ b/SwiftyStoreKit.xcodeproj/project.pbxproj
@@ -7,6 +7,9 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
+		1592CD501E27756500D321E6 /* ReceiptValidators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1592CD4F1E27756500D321E6 /* ReceiptValidators.swift */; };
+		1592CD511E27756500D321E6 /* ReceiptValidators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1592CD4F1E27756500D321E6 /* ReceiptValidators.swift */; };
+		1592CD521E27756500D321E6 /* ReceiptValidators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1592CD4F1E27756500D321E6 /* ReceiptValidators.swift */; };
 		54B069911CF742CE00BAFE38 /* InAppCompleteTransactionsObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 651A71241CD651AF000B4091 /* InAppCompleteTransactionsObserver.swift */; };
 		54B069921CF742D100BAFE38 /* InAppReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A7C7621C29B8D00053ED64 /* InAppReceipt.swift */; };
 		54B069931CF742D300BAFE38 /* InAppReceiptRefreshRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4083C561C2AB0A900295248 /* InAppReceiptRefreshRequest.swift */; };
@@ -96,6 +99,7 @@
 /* End PBXCopyFilesBuildPhase section */
 
 /* Begin PBXFileReference section */
+		1592CD4F1E27756500D321E6 /* ReceiptValidators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiptValidators.swift; sourceTree = "<group>"; };
 		54C0D52C1CF7404500F90BCE /* SwiftyStoreKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftyStoreKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		6502F5FE1B985833004E342D /* SwiftyStoreKit_iOSDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftyStoreKit_iOSDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		6502F6221B98586A004E342D /* InAppProductPurchaseRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppProductPurchaseRequest.swift; sourceTree = "<group>"; };
@@ -200,6 +204,7 @@
 				6502F6231B98586A004E342D /* InAppProductQueryRequest.swift */,
 				C4083C561C2AB0A900295248 /* InAppReceiptRefreshRequest.swift */,
 				C4A7C7621C29B8D00053ED64 /* InAppReceipt.swift */,
+				1592CD4F1E27756500D321E6 /* ReceiptValidators.swift */,
 				651A71241CD651AF000B4091 /* InAppCompleteTransactionsObserver.swift */,
 				653722801DB8282600C8F944 /* SKProduct+LocalizedPrice.swift */,
 				6502F6241B98586A004E342D /* SwiftyStoreKit.swift */,
@@ -473,6 +478,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				54B069911CF742CE00BAFE38 /* InAppCompleteTransactionsObserver.swift in Sources */,
+				1592CD521E27756500D321E6 /* ReceiptValidators.swift in Sources */,
 				54B069951CF742D900BAFE38 /* InAppProductPurchaseRequest.swift in Sources */,
 				54C0D5681CF7428400F90BCE /* SwiftyStoreKit.swift in Sources */,
 				54B069961CF744DC00BAFE38 /* OS.swift in Sources */,
@@ -499,6 +505,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				C40C68101C29414C00B60B7E /* OS.swift in Sources */,
+				1592CD501E27756500D321E6 /* ReceiptValidators.swift in Sources */,
 				651A71251CD651AF000B4091 /* InAppCompleteTransactionsObserver.swift in Sources */,
 				6502F63A1B985C9E004E342D /* InAppProductPurchaseRequest.swift in Sources */,
 				6502F63B1B985CA1004E342D /* InAppProductQueryRequest.swift in Sources */,
@@ -515,6 +522,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				C40C68111C29419500B60B7E /* OS.swift in Sources */,
+				1592CD511E27756500D321E6 /* ReceiptValidators.swift in Sources */,
 				651A71261CD651AF000B4091 /* InAppCompleteTransactionsObserver.swift in Sources */,
 				C4D74BC31C24CEDC0071AD3E /* InAppProductPurchaseRequest.swift in Sources */,
 				C4D74BC41C24CEDC0071AD3E /* InAppProductQueryRequest.swift in Sources */,
diff --git a/SwiftyStoreKit/InAppReceipt.swift b/SwiftyStoreKit/InAppReceipt.swift
index 95b5c9fb..d9dde9b7 100644
--- a/SwiftyStoreKit/InAppReceipt.swift
+++ b/SwiftyStoreKit/InAppReceipt.swift
@@ -53,9 +53,8 @@ internal class InAppReceipt {
      *  - Parameter completion: handler for result
      */
     class func verify(
-        urlType: VerifyReceiptURLType = .production,
+		using validator: ReceiptValidator,
         password autoRenewPassword: String? = nil,
-        session: URLSession = URLSession.shared,
         completion: @escaping (VerifyReceiptResult) -> ()) {
 
             // If no receipt is present, validation fails.
diff --git a/SwiftyStoreKit/SwiftyStoreKit.swift b/SwiftyStoreKit/SwiftyStoreKit.swift
index 7742f274..a62e8f2c 100644
--- a/SwiftyStoreKit/SwiftyStoreKit.swift
+++ b/SwiftyStoreKit/SwiftyStoreKit.swift
@@ -143,10 +143,11 @@ public class SwiftyStoreKit {
      *  - Parameter completion: handler for result
      */
     public class func verifyReceipt(
+		using validator: ReceiptValidator,
         password: String? = nil,
-        session: URLSession = URLSession.shared,
         completion:@escaping (VerifyReceiptResult) -> ()) {
-        InAppReceipt.verify(urlType: .production, password: password, session: session) { result in
+
+		InAppReceipt.verify(using: validator, password: password) { result in
          
             DispatchQueue.main.async {
                 completion(result)

From 03d34bab6919c56108d6ee7773f011e93fa9a61e Mon Sep 17 00:00:00 2001
From: Luong Huy Duc <luonghuyduc88@gmail.com>
Date: Thu, 12 Jan 2017 17:02:46 +0800
Subject: [PATCH 4/4] Pass in AppleReceiptValidator object in Examples

---
 SwiftyStoreKit-iOS-Demo/AppDelegate.swift    | 5 +++--
 SwiftyStoreKit-iOS-Demo/ViewController.swift | 8 +++++---
 2 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/SwiftyStoreKit-iOS-Demo/AppDelegate.swift b/SwiftyStoreKit-iOS-Demo/AppDelegate.swift
index d71c7c1d..3a77bef6 100644
--- a/SwiftyStoreKit-iOS-Demo/AppDelegate.swift
+++ b/SwiftyStoreKit-iOS-Demo/AppDelegate.swift
@@ -40,8 +40,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
     }
     
     func verifyReceipt() {
-        
-        SwiftyStoreKit.verifyReceipt(password: "your-shared-secret") { result in
+
+		let appleValidator = AppleReceiptValidator(service: .production)
+		SwiftyStoreKit.verifyReceipt(using: appleValidator, password: "your-shared-secret") { result in
             switch result {
             case .success(let receipt):
                 print("\(receipt)")
diff --git a/SwiftyStoreKit-iOS-Demo/ViewController.swift b/SwiftyStoreKit-iOS-Demo/ViewController.swift
index ced351ad..f62fd472 100644
--- a/SwiftyStoreKit-iOS-Demo/ViewController.swift
+++ b/SwiftyStoreKit-iOS-Demo/ViewController.swift
@@ -109,7 +109,8 @@ class ViewController: UIViewController {
     @IBAction func verifyReceipt() {
 
         NetworkActivityIndicatorManager.networkOperationStarted()
-        SwiftyStoreKit.verifyReceipt(password: "your-shared-secret") { result in
+		let appleValidator = AppleReceiptValidator(service: .production)
+		SwiftyStoreKit.verifyReceipt(using: appleValidator, password: "your-shared-secret") { result in
             NetworkActivityIndicatorManager.networkOperationFinished()
 
             self.showAlert(self.alertForVerifyReceipt(result))
@@ -123,9 +124,10 @@ class ViewController: UIViewController {
     }
 
     func verifyPurchase(_ purchase: RegisteredPurchase) {
-     
+
         NetworkActivityIndicatorManager.networkOperationStarted()
-        SwiftyStoreKit.verifyReceipt(password: "your-shared-secret") { result in
+		let appleValidator = AppleReceiptValidator(service: .production)
+		SwiftyStoreKit.verifyReceipt(using: appleValidator, password: "your-shared-secret") { result in
             NetworkActivityIndicatorManager.networkOperationFinished()
             
             switch result {