在Swift 3中实现收据validation

我正在Swift 3中开发一个iOS应用程序,并尝试按照本教程实现收据validation: http : //savvyapps.com/blog/how-setup-test-auto-renewable-subscription-ios-app 。 但是,该教程似乎是使用早期版本的Swift编写的,因此我不得不进行一些更改。 这是我的receiptValidation()函数:

func receiptValidation() { let receiptPath = Bundle.main.appStoreReceiptURL?.path if FileManager.default.fileExists(atPath: receiptPath!){ var receiptData:NSData? do{ receiptData = try NSData(contentsOf: Bundle.main.appStoreReceiptURL!, options: NSData.ReadingOptions.alwaysMapped) } catch{ print("ERROR: " + error.localizedDescription) } let receiptString = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) let postString = "receipt-data=" + receiptString! + "&password=" + SUBSCRIPTION_SECRET let storeURL = NSURL(string:"https://sandbox.itunes.apple.com/verifyReceipt")! let storeRequest = NSMutableURLRequest(url: storeURL as URL) storeRequest.httpMethod = "POST" storeRequest.httpBody = postString.data(using: .utf8) let session = URLSession(configuration:URLSessionConfiguration.default) let task = session.dataTask(with: storeRequest as URLRequest) { data, response, error in do{ let jsonResponse:NSDictionary = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary let expirationDate:NSDate = self.expirationDateFromResponse(jsonResponse: jsonResponse)! self.updateIAPExpirationDate(date: expirationDate) } catch{ print("ERROR: " + error.localizedDescription) } } task.resume() } } 

当我尝试调用expirationDateFromResponse()方法时出现问题。 事实certificate,传递给此方法的jsonResponse只包含: status = 21002; 。 我查了一下,这意味着“收据数据属性中的数据格式错误或丢失。” 但是,我正在测试的设备有一个活动的沙盒订阅产品,除了这个问题,订阅似乎正常工作。 还有什么我还需要做的事情,以确保将正确读取和编码receiptData值,或者可能导致此问题的其他一些问题?

编辑:

我尝试了另一种设置storeRequest.httpBody的方法:

 func receiptValidation() { let receiptPath = Bundle.main.appStoreReceiptURL?.path if FileManager.default.fileExists(atPath: receiptPath!){ var receiptData:NSData? do{ receiptData = try NSData(contentsOf: Bundle.main.appStoreReceiptURL!, options: NSData.ReadingOptions.alwaysMapped) } catch{ print("ERROR: " + error.localizedDescription) } let receiptString = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) //.URLEncoded let dict = ["receipt-data":receiptString, "password":SUBSCRIPTION_SECRET] as [String : Any] var jsonData:Data? do{ jsonData = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted) } catch{ print("ERROR: " + error.localizedDescription) } let storeURL = NSURL(string:"https://sandbox.itunes.apple.com/verifyReceipt")! let storeRequest = NSMutableURLRequest(url: storeURL as URL) storeRequest.httpMethod = "POST" storeRequest.httpBody = jsonData! let session = URLSession(configuration:URLSessionConfiguration.default) let task = session.dataTask(with: storeRequest as URLRequest) { data, response, error in do{ let jsonResponse:NSDictionary = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary let expirationDate:NSDate = self.expirationDateFromResponse(jsonResponse: jsonResponse)! self.updateIAPExpirationDate(date: expirationDate) } catch{ print("ERROR: " + error.localizedDescription) } } task.resume() } } 

但是,当我使用此代码运行应用程序时,它会在到达行jsonData = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted) 。 它甚至没有进入捕获块,它只是停止做任何事情。 从我在网上看到的,其他人似乎在使用JSONSerialization.data在Swift 3中设置请求httpBody时遇到了麻烦。

它与Swift 4一起正常工作

 func receiptValidation() { let SUBSCRIPTION_SECRET = "yourpasswordift" let receiptPath = Bundle.main.appStoreReceiptURL?.path if FileManager.default.fileExists(atPath: receiptPath!){ var receiptData:NSData? do{ receiptData = try NSData(contentsOf: Bundle.main.appStoreReceiptURL!, options: NSData.ReadingOptions.alwaysMapped) } catch{ print("ERROR: " + error.localizedDescription) } //let receiptString = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) let base64encodedReceipt = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions.endLineWithCarriageReturn) print(base64encodedReceipt!) let requestDictionary = ["receipt-data":base64encodedReceipt!,"password":SUBSCRIPTION_SECRET] guard JSONSerialization.isValidJSONObject(requestDictionary) else { print("requestDictionary is not valid JSON"); return } do { let requestData = try JSONSerialization.data(withJSONObject: requestDictionary) let validationURLString = "https://sandbox.itunes.apple.com/verifyReceipt" // this works but as noted above it's best to use your own trusted server guard let validationURL = URL(string: validationURLString) else { print("the validation url could not be created, unlikely error"); return } let session = URLSession(configuration: URLSessionConfiguration.default) var request = URLRequest(url: validationURL) request.httpMethod = "POST" request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringCacheData let task = session.uploadTask(with: request, from: requestData) { (data, response, error) in if let data = data , error == nil { do { let appReceiptJSON = try JSONSerialization.jsonObject(with: data) print("success. here is the json representation of the app receipt: \(appReceiptJSON)") // if you are using your server this will be a json representation of whatever your server provided } catch let error as NSError { print("json serialization failed with error: \(error)") } } else { print("the upload task returned an error: \(error)") } } task.resume() } catch let error as NSError { print("json serialization failed with error: \(error)") } } } 

我更新了@ user3726962的代码,删除了不必要的NS’es和“崩溃运算符”。 现在看起来应该更像Swift 3

在使用此代码之前,请注意Apple不建议直接[设备] < - > [Apple服务器]validation并要求执行[设备] < - > [您的服务器] < - > [Apple服务器]。 仅在您不怕将应用程序内购买入侵时使用。

更新:使function通用:它将首先尝试使用Productionvalidation收据,如果失败 – 它将重复使用Sandbox。 它有点笨重,但应该是独立的,独立于第三方。

 func tryCheckValidateReceiptAndUpdateExpirationDate() { if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) { NSLog("^A receipt found. Validating it...") GlobalVariables.isPremiumInAmbiquousState = true // We will allow user to use all premium features until receipt is validated // If we have problems validating the purchase - this is not user's fault do { let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped) let receiptString = receiptData.base64EncodedString(options: []) let dict = ["receipt-data" : receiptString, "password" : "your_shared_secret"] as [String : Any] do { let jsonData = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted) if let storeURL = Foundation.URL(string:"https://buy.itunes.apple.com/verifyReceipt"), let sandboxURL = Foundation.URL(string: "https://sandbox.itunes.apple.com/verifyReceipt") { var request = URLRequest(url: storeURL) request.httpMethod = "POST" request.httpBody = jsonData let session = URLSession(configuration: URLSessionConfiguration.default) NSLog("^Connecting to production...") let task = session.dataTask(with: request) { data, response, error in // BEGIN of closure #1 - verification with Production if let receivedData = data, let httpResponse = response as? HTTPURLResponse, error == nil, httpResponse.statusCode == 200 { NSLog("^Received 200, verifying data...") do { if let jsonResponse = try JSONSerialization.jsonObject(with: receivedData, options: JSONSerialization.ReadingOptions.mutableContainers) as? Dictionary, let status = jsonResponse["status"] as? Int64 { switch status { case 0: // receipt verified in Production NSLog("^Verification with Production succesful, updating expiration date...") self.updateExpirationDate(jsonResponse: jsonResponse) // Leaves isPremiumInAmbiquousState=true if fails case 21007: // Means that our receipt is from sandbox environment, need to validate it there instead NSLog("^need to repeat evrything with Sandbox") var request = URLRequest(url: sandboxURL) request.httpMethod = "POST" request.httpBody = jsonData let session = URLSession(configuration: URLSessionConfiguration.default) NSLog("^Connecting to Sandbox...") let task = session.dataTask(with: request) { data, response, error in // BEGIN of closure #2 - verification with Sandbox if let receivedData = data, let httpResponse = response as? HTTPURLResponse, error == nil, httpResponse.statusCode == 200 { NSLog("^Received 200, verifying data...") do { if let jsonResponse = try JSONSerialization.jsonObject(with: receivedData, options: JSONSerialization.ReadingOptions.mutableContainers) as? Dictionary, let status = jsonResponse["status"] as? Int64 { switch status { case 0: // receipt verified in Sandbox NSLog("^Verification succesfull, updating expiration date...") self.updateExpirationDate(jsonResponse: jsonResponse) // Leaves isPremiumInAmbiquousState=true if fails default: self.showAlertWithErrorCode(errorCode: status) } } else { DebugLog("Failed to cast serialized JSON to Dictionary") } } catch { DebugLog("Couldn't serialize JSON with error: " + error.localizedDescription) } } else { self.handleNetworkError(data: data, response: response, error: error) } } // END of closure #2 = verification with Sandbox task.resume() default: self.showAlertWithErrorCode(errorCode: status) } } else { DebugLog("Failed to cast serialized JSON to Dictionary") } } catch { DebugLog("Couldn't serialize JSON with error: " + error.localizedDescription) } } else { self.handleNetworkError(data: data, response: response, error: error) } } // END of closure #1 - verification with Production task.resume() } else { DebugLog("Couldn't convert string into URL. Check for special characters.") } } catch { DebugLog("Couldn't create JSON with error: " + error.localizedDescription) } } catch { DebugLog("Couldn't read receipt data with error: " + error.localizedDescription) } } else { DebugLog("No receipt found even though there is an indication something has been purchased before") NSLog("^No receipt found. Need to refresh receipt.") self.refreshReceipt() } } 

这适用于自动续订订阅。 尚未使用其他类型的订阅进行测试。 如果它适用于某些其他订阅类型,请发表评论。

同样的问题,我挣扎着。 问题是这一行:

 let receiptString = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) 

返回一个OPTIONAL和

 jsonData = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted) 

无法处理期权。 所以要修复它,只需用第一行代码替换:

 let receiptString:String = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions.lineLength64Characters) as String! 

一切都会像魅力一样!

最终我通过让我的应用程序调用用Python编写的Lambda函数来解决问题,如本答案所示。 我仍然不确定我的Swift代码有什么问题,或者如何在Swift 3中完全做到这一点,但Lambda函数在任何情况下都得到了理想的结果。

//评论太低了

Yasin Aktimur,谢谢你的回答,真棒。 但是,看看Apple的文档,他们说要在一个单独的Queue上连接到iTunes。 所以看起来应该是这样的:

 func receiptValidation() { let SUBSCRIPTION_SECRET = "secret" let receiptPath = Bundle.main.appStoreReceiptURL?.path if FileManager.default.fileExists(atPath: receiptPath!){ var receiptData:NSData? do{ receiptData = try NSData(contentsOf: Bundle.main.appStoreReceiptURL!, options: NSData.ReadingOptions.alwaysMapped) } catch{ print("ERROR: " + error.localizedDescription) } let base64encodedReceipt = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions.endLineWithCarriageReturn) let requestDictionary = ["receipt-data":base64encodedReceipt!,"password":SUBSCRIPTION_SECRET] guard JSONSerialization.isValidJSONObject(requestDictionary) else { print("requestDictionary is not valid JSON"); return } do { let requestData = try JSONSerialization.data(withJSONObject: requestDictionary) let validationURLString = "https://sandbox.itunes.apple.com/verifyReceipt" // this works but as noted above it's best to use your own trusted server guard let validationURL = URL(string: validationURLString) else { print("the validation url could not be created, unlikely error"); return } let session = URLSession(configuration: URLSessionConfiguration.default) var request = URLRequest(url: validationURL) request.httpMethod = "POST" request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringCacheData let queue = DispatchQueue(label: "itunesConnect") queue.async { let task = session.uploadTask(with: request, from: requestData) { (data, response, error) in if let data = data , error == nil { do { let appReceiptJSON = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? NSDictionary print("success. here is the json representation of the app receipt: \(appReceiptJSON)") } catch let error as NSError { print("json serialization failed with error: \(error)") } } else { print("the upload task returned an error: \(error ?? "couldn't upload" as! Error)") } } task.resume() } } catch let error as NSError { print("json serialization failed with error: \(error)") } } } 

我喜欢你的答案,我只是在C#中重写了那些像我一样使用它的人,因为我没有找到解决方案的良好来源。 再次感谢耗材IAP

 void ReceiptValidation() { var recPath = NSBundle.MainBundle.AppStoreReceiptUrl.Path; if (File.Exists(recPath)) { NSData recData; NSError error; recData = NSData.FromUrl(NSBundle.MainBundle.AppStoreReceiptUrl, NSDataReadingOptions.MappedAlways, out error); var recString = recData.GetBase64EncodedString(NSDataBase64EncodingOptions.None); var dict = new Dictionary(); dict.TryAdd("receipt-data", recString); var dict1 = NSDictionary.FromObjectsAndKeys(dict.Values.ToArray(), dict.Keys.ToArray()); var storeURL = new NSUrl("https://sandbox.itunes.apple.com/verifyReceipt"); var storeRequest = new NSMutableUrlRequest(storeURL); storeRequest.HttpMethod = "POST"; var jsonData = NSJsonSerialization.Serialize(dict1, NSJsonWritingOptions.PrettyPrinted, out error); if (error == null) { storeRequest.Body = jsonData; var session = NSUrlSession.FromConfiguration(NSUrlSessionConfiguration.DefaultSessionConfiguration); var tsk = session.CreateDataTask(storeRequest, (data, response, err) => { if (err == null) { var rstr = NSJsonSerialization.FromObject(data); } else { // Check Error } }); tsk.Resume(); }else { // JSON Error Handling } } }