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 { 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 = ""; }; 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 = ""; }; @@ -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 3395b3e8..d9dde9b7 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 } @@ -59,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. @@ -70,80 +63,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() + } +} 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 } 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)