web-dev-qa-db-ja.com

Swift 3での領収書検証の実装

私はSwift 3でiOSアプリを開発しており、このチュートリアルに従ってレシート検証を実装しようとしています: http://savvyapps.com/blog/how-setup-test-auto- refreshable-subscription-ios-app 。ただし、チュートリアルは以前のバージョンのSwiftを使用して作成されているようであるため、いくつかの変更を加える必要がありました。receiveValidation()関数を次に示します。

_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()メソッドを呼び出そうとすると発生します。このメソッドに渡されるjsonResponseには、_status = 21002;_のみが含まれていることがわかります。これを調べたところ、「receive-dataプロパティのデータの形式が正しくないか欠落している」という意味です。ただし、テストしているデバイスには、製品のアクティブなサンドボックスサブスクリプションがあり、サブスクリプションはこの問題を除けば正しく機能しているようです。 receiveData値が正しく読み取られてエンコードされることを確認するために、他に何かする必要があること、またはこの問題を引き起こしている可能性のある他の問題はありますか?

編集:

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を使用してリクエストhttpBodyをSwift 3に設定するのに問題があるようです。

6
user3726962

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)")
        }



    }
}
10
Yasin Aktimur

@ user3726962のコードを更新し、不要なNSと「クラッシュ演算子」を削除しました。 Swiftのようになります。

このコードを使用する前に、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()
}

これは、自動更新可能なサブスクリプションで機能します。他の種類のサブスクリプションではまだテストしていません。他のサブスクリプションタイプで機能する場合は、コメントを残してください。

7
Vitalii

//コメントするには担当者が少なすぎる

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)")
        }
    }
}
3
Not Batman

私は同じ問題で頭を悩ませました。問題は、次の行です。

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

オプションを返し、

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

オプションは処理できません。したがって、これを修正するには、コードの最初の行を次のように置き換えるだけです。

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

そして、すべてが魅力のように機能します!

2
Pablo Romeu

最終的には、 this answerに示すように、アプリでPythonで記述されたLambda関数を呼び出すことで問題を解決することができました。 Swiftコードの何が問題だったのか、またはSwift 3でこれを完全に行う方法はまだわかりませんが、Lambda関数はとにかく。

0
user3726962

私はあなたの答えが好きで、解決策の良い情報源が見つからなかったので、私のようにそれを使用している人のために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
            }
        }
    }
0