【问题标题】:Implementing Receipt Validation in Swift 3在 Swift 3 中实现收据验证
【发布时间】:2017-02-04 07:11:33
【问题描述】:

我正在使用 Swift 3 开发一个 iOS 应用程序,并尝试按照本教程实现收据验证: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()
    }
}

当我尝试调用 expireDateFromResponse() 方法时,问题就出现了。事实证明,传递给此方法的 jsonResponse 仅包含:status = 21002;。我查了一下,它的意思是“receipt-data 属性中的数据格式错误或丢失。”但是,我正在测试的设备具有该产品的有效沙盒订阅,并且订阅似乎除了这个问题之外可以正常工作。我还需要做些什么来确保正确读取和编码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) 行时挂起。它甚至没有到达 catch 块,它只是停止做任何事情。从我在网上看到的情况来看,其他人似乎无法在 Swift 3 中使用 JSONSerialization.data 设置请求 httpBody。

【问题讨论】:

  • 确保您在 base64 编码收据中对任何 + 字符进行了 % 编码。即用 %2b 替换 + 的实例
  • 我在声明后立即更改了代码以对receiptString 进行此更改,但我仍然看到相同的错误。另外,当我打印出receiptString 时,我注意到它包含很多“/”字符来分隔长的base 64 字符串。这是正确编码后的样子吗?
  • 我还应该提到我尝试删除“/”字符,但我仍然看到 21002 状态。
  • 我更新了我的 Gist 以显示我用来检索收据和编码 base64 数据的代码。在我的情况下,这将发送到我的 php 代码,然后将其发送到 Apple 的服务器 gist.github.com/paulw11/fa76e10f785e055338ce06673787c6d2
  • 查看您的代码,您发送的数据不正确。您将收据和密码作为 POST 数据发送,但您需要发送一个包含收据和密码的 JSON 对象。如果你这样做了,那么你不需要担心 % 编码,这是我需要它来使用我的 PHP 的东西。

标签: ios swift in-app-purchase swift3 receipt-validation


【解决方案1】:

它可以在 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)")
        }



    }
}

【讨论】:

  • 这段代码放在哪里?在 AppDelegate.swift 中?因为在我的 ViewController.swift 中我找不到让它工作的方法。
  • 这只是为了自动更新吗?还是经常在应用内购买?
  • 你的密码是什么?
  • yourpasswordift 是您的 AppSecretKey 通过 iTunes 连接在应用内购买设置页面下找到它。
【解决方案2】:

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

在使用此代码之前,请注意 Apple 不建议直接进行 [设备] [Apple 服务器] 验证,并要求进行 [设备] [您的服务器] [Apple 服务器] .仅在您不害怕应用内购买被黑客入侵时使用。

更新:使该功能具有通用性:它将首先尝试使用 Production 验证收据,如果失败 - 它将使用 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<String, AnyObject>,
                                    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<String, AnyObject>,
                                                            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<String, AnyObject>") }
                                                    }
                                                    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<String, AnyObject>") }
                            }
                            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()
    }
}

func refreshReceipt() {
    let request = SKReceiptRefreshRequest()
    request.delegate = self // to be able to receive the results of this request, check the SKRequestDelegate protocol
    request.start()
}

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

【讨论】:

  • 这段代码放在哪里?在 AppDelegate.swift 中?因为在我的 ViewController.swift 中我找不到让它工作的方法。
  • @GhiggzPikkoro 它是一个独立的代码,可以放在任何地方。在我的例子中,它是 IAPHelper 类的一部分,而它又被保存为名为“store”的 IAPProducts 结构的静态实例。因此,只要我需要,它就会从 AppDelegate 被称为 IAPProducts.store.tryCheckValidateReceiptAndUpdateExpirationDate()。
  • 哦,是的,我明白了,我把它放在我创建的 IAP 服务类中,并在我想要的对象上的任何地方调用此方法。谢谢它现在有效
  • 最后一个问题,当我将我的应用程序提交到 App Store 时,将其设置为生产模式,我是否应该将以下 URL 更改为:sandbox.itunes.apple.com/verifyReceipt 为:buy.itunes.apple.com/verifyReceipt。以其他方式,在将我的应用程序放到 AppStore 之前,我应该在这个 url 中通过“购买”来更改“沙盒”吗?
  • @GhiggzPikkoro 好问题。简短的回答是肯定的。但是您也可以在此处查看我的答案中的更新代码,它会处理“沙盒与生产”问题,期待并处理 21007 代码,这意味着“收据来自沙盒,去那里检查吧,伙计”。附言抱歉,它仍在 Swift 3 上,希望它可以转换为 4。
【解决方案3】:

//代表太低,无法评论

Yasin Aktimur,感谢您的回答,太棒了。但是,查看有关此的 Apple 文档,他们说要在单独的队列上连接到 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)")
        }
    }
}

【讨论】:

  • 这段代码放在哪里?在 AppDelegate.swift 中?因为在我的 ViewController.swift 中我找不到让它工作的方法。
  • 好吧,我应该问我的朋友谷歌,帮我做一个句子,我的英语太差了,翻译无法帮助我
  • 你会从多个类调用函数吗?如果是这样,请将此功能公开在一个单独的文件中,以保持一切清洁。如果不只是把它放在你的课堂上并在购买后调用它
【解决方案4】:

我为同样的问题苦苦挣扎。问题是这一行:

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!

一切都会像魅力一样发挥作用!

【讨论】:

  • 这很奇怪,但是您的解决方案会返回错误 21002,而问题中给出的原始代码可以正常工作(即使收据字符串是可选的)。我没有反对,因为根据购买类型或其他我不知道的东西可能会有一些细微的差异,所以有人可能仍然会觉得这个答案很有帮助。
  • @VitaliiTymoshenko 在原始问题中,receiptData 是可选的,它不能传递给 JSONSerialization.data(...)。也许你的问题是另一个问题。
  • 您建议的代码使用 .lineLength64Characters。我已经仔细检查了使用此选项从数据中获取字符串是否会为我产生错误 21002。我同意原始代码具有可选(我在调试时清楚地看到了它),但我也很惊讶它以某种方式对我正常工作。所以我最终使用了默认的编码方法——receiptData.base64EncodedString(options: [])。请参阅下面我的答案中的完整代码。
  • 这段代码放在哪里?在 AppDelegate.swift 中?因为在我的 ViewController.swift 中我找不到让它工作的方法。
  • @GhiggzPikkoro 阅读原始问题。原始代码位于:savvyapps.com/blog/…
【解决方案5】:

我喜欢你的答案,我只是用 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<String,String>();
            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
            }
        }
    }

【讨论】:

    【解决方案6】:

    最终我能够通过让我的应用调用用 Python 编写的 Lambda 函数来解决问题,如this 答案所示。我仍然不确定我的 Swift 代码出了什么问题,或者如何在 Swift 3 中完全做到这一点,但无论如何 Lambda 函数都得到了预期的结果。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-11-24
      • 1970-01-01
      • 1970-01-01
      • 2015-01-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-02-21
      相关资源
      最近更新 更多